--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit f23855fb6854e46a5fa4e6cf0be2bcdab4f67547
Parents : 25f0d52
Author : Mark Qvist <mark@unsigned.io>
Date : 2022-09-16T18:07:57+02:00
Included local plyer
Changes
166 files changed, 15862 insertions(+), 0 deletions(-)
Diff
diff --git a/sbapp/plyer/__init__.py b/sbapp/plyer/__init__.py
new file mode 100644
index 00000000..ab87901c
--- /dev/null
+++ b/sbapp/plyer/__init__.py
@@ -0,0 +1,124 @@
+'''
+Plyer
+=====
+
+'''
+
+__all__ = (
+ 'accelerometer', 'audio', 'barometer', 'battery', 'bluetooth',
+ 'brightness', 'call', 'camera', 'compass', 'cpu', 'email', 'filechooser',
+ 'flash', 'gps', 'gravity', 'gyroscope', 'humidity', 'irblaster',
+ 'keystore', 'light', 'notification', 'orientation', 'processors',
+ 'proximity', 'screenshot', 'sms', 'spatialorientation', 'storagepath',
+ 'stt', 'temperature', 'tts', 'uniqueid', 'vibrator', 'wifi', 'devicename'
+)
+
+__version__ = '2.1.0.dev0'
+
+
+from plyer import facades
+from plyer.utils import Proxy
+
+#: Accelerometer proxy to :class:`plyer.facades.Accelerometer`
+accelerometer = Proxy('accelerometer', facades.Accelerometer)
+
+#: Keyring proxy to :class::`plyer.facades.Keystore`
+keystore = Proxy('keystore', facades.Keystore)
+
+#: Audio proxy to :class:`plyer.facades.Audio`
+audio = Proxy('audio', facades.Audio)
+
+#: Barometer proxy to :class:`plyer.facades.Barometer`
+barometer = Proxy('barometer', facades.Barometer)
+
+#: Battery proxy to :class:`plyer.facades.Battery`
+battery = Proxy('battery', facades.Battery)
+
+#: Call proxy to :class `plyer.facades.Call`
+call = Proxy('call', facades.Call)
+
+#: Compass proxy to :class:`plyer.facades.Compass`
+compass = Proxy('compass', facades.Compass)
+
+#: Camera proxy to :class:`plyer.facades.Camera`
+camera = Proxy('camera', facades.Camera)
+
+#: Email proxy to :class:`plyer.facades.Email`
+email = Proxy('email', facades.Email)
+
+#: FileChooser proxy to :class:`plyer.facades.FileChooser`
+filechooser = Proxy('filechooser', facades.FileChooser)
+
+#: GPS proxy to :class:`plyer.facades.GPS`
+gps = Proxy('gps', facades.GPS)
+
+#: Gravity proxy to :class:`plyer.facades.Gravity`
+gravity = Proxy('gravity', facades.Gravity)
+
+#: Gyroscope proxy to :class:`plyer.facades.Gyroscope`
+gyroscope = Proxy('gyroscope', facades.Gyroscope)
+
+#: IrBlaster proxy to :class:`plyer.facades.IrBlaster`
+irblaster = Proxy('irblaster', facades.IrBlaster)
+
+#: Light proxy to :class:`plyer.facades.Light`
+light = Proxy('light', facades.Light)
+
+#: Orientation proxy to :class:`plyer.facades.Orientation`
+orientation = Proxy('orientation', facades.Orientation)
+
+#: Notification proxy to :class:`plyer.facades.Notification`
+notification = Proxy('notification', facades.Notification)
+
+#: Proximity proxy to :class:`plyer.facades.Proximity`
+proximity = Proxy('proximity', facades.Proximity)
+
+#: Sms proxy to :class:`plyer.facades.Sms`
+sms = Proxy('sms', facades.Sms)
+
+#: Speech proxy to :class:`plyer.facades.STT`
+stt = Proxy('stt', facades.STT)
+
+#: TTS proxy to :class:`plyer.facades.TTS`
+tts = Proxy('tts', facades.TTS)
+
+#: UniqueID proxy to :class:`plyer.facades.UniqueID`
+uniqueid = Proxy('uniqueid', facades.UniqueID)
+
+#: Vibrator proxy to :class:`plyer.facades.Vibrator`
+vibrator = Proxy('vibrator', facades.Vibrator)
+
+#: Flash proxy to :class:`plyer.facades.Flash`
+flash = Proxy('flash', facades.Flash)
+
+#: Wifi proxy to :class:`plyer.facades.Wifi`
+wifi = Proxy('wifi', facades.Wifi)
+
+#: Temperature proxy to :class:`plyer.facades.Temperature`
+temperature = Proxy('temperature', facades.Temperature)
+
+#: Humidity proxy to :class:`plyer.facades.Humidity`
+humidity = Proxy('humidity', facades.Humidity)
+#: SpatialOrientation proxy to :class:`plyer.facades.SpatialOrientation`
+spatialorientation = Proxy('spatialorientation', facades.SpatialOrientation)
+
+#: Brightness proxy to :class:`plyer.facades.Brightness`
+brightness = Proxy('brightness', facades.Brightness)
+
+#: StoragePath proxy to :class:`plyer.facades.StoragePath`
+storagepath = Proxy('storagepath', facades.StoragePath)
+
+#: Bluetooth proxy to :class:`plyer.facades.Bluetooth`
+bluetooth = Proxy('bluetooth', facades.Bluetooth)
+
+#: Processors proxy to :class:`plyer.facades.Processors`
+processors = Proxy('processors', facades.Processors)
+
+#: Processors proxy to :class:`plyer.facades.CPU`
+cpu = Proxy('cpu', facades.CPU)
+
+#: Screenshot proxy to :class:`plyer.facades.Screenshot`
+screenshot = Proxy('screenshot', facades.Screenshot)
+
+#: devicename proxy to :class:`plyer.facades.DeviceName`
+devicename = Proxy('devicename', facades.DeviceName)
diff --git a/sbapp/plyer/facades/__init__.py b/sbapp/plyer/facades/__init__.py
new file mode 100644
index 00000000..c1e25605
--- /dev/null
+++ b/sbapp/plyer/facades/__init__.py
@@ -0,0 +1,51 @@
+'''
+Facades
+=======
+
+Interface of all the features available.
+
+'''
+
+__all__ = ('Accelerometer', 'Audio', 'Barometer', 'Battery', 'Call', 'Camera',
+ 'Compass', 'Email', 'FileChooser', 'GPS', 'Gravity', 'Gyroscope',
+ 'IrBlaster', 'Light', 'Orientation', 'Notification', 'Proximity',
+ 'Sms', 'TTS', 'UniqueID', 'Vibrator', 'Wifi', 'Flash', 'CPU',
+ 'Temperature', 'Humidity', 'SpatialOrientation', 'Brightness',
+ 'Processors', 'StoragePath', 'Keystore', 'Bluetooth', 'Screenshot',
+ 'STT', 'DeviceName')
+
+from plyer.facades.accelerometer import Accelerometer
+from plyer.facades.audio import Audio
+from plyer.facades.barometer import Barometer
+from plyer.facades.battery import Battery
+from plyer.facades.call import Call
+from plyer.facades.camera import Camera
+from plyer.facades.compass import Compass
+from plyer.facades.email import Email
+from plyer.facades.filechooser import FileChooser
+from plyer.facades.flash import Flash
+from plyer.facades.gps import GPS
+from plyer.facades.gravity import Gravity
+from plyer.facades.gyroscope import Gyroscope
+from plyer.facades.irblaster import IrBlaster
+from plyer.facades.light import Light
+from plyer.facades.proximity import Proximity
+from plyer.facades.orientation import Orientation
+from plyer.facades.notification import Notification
+from plyer.facades.sms import Sms
+from plyer.facades.stt import STT
+from plyer.facades.tts import TTS
+from plyer.facades.uniqueid import UniqueID
+from plyer.facades.vibrator import Vibrator
+from plyer.facades.wifi import Wifi
+from plyer.facades.temperature import Temperature
+from plyer.facades.humidity import Humidity
+from plyer.facades.spatialorientation import SpatialOrientation
+from plyer.facades.brightness import Brightness
+from plyer.facades.keystore import Keystore
+from plyer.facades.storagepath import StoragePath
+from plyer.facades.bluetooth import Bluetooth
+from plyer.facades.processors import Processors
+from plyer.facades.cpu import CPU
+from plyer.facades.screenshot import Screenshot
+from plyer.facades.devicename import DeviceName
diff --git a/sbapp/plyer/facades/accelerometer.py b/sbapp/plyer/facades/accelerometer.py
new file mode 100644
index 00000000..d8a7143c
--- /dev/null
+++ b/sbapp/plyer/facades/accelerometer.py
@@ -0,0 +1,75 @@
+'''
+Accelerometer
+============
+
+The accelerometer is a motion sensor that detects the change (delta) in
+movement relative to the current device orientation, in three dimensions
+along the x, y, and z axis.
+
+The :class:`Accelerometer` provides access to public methods to
+use accelerometer of your device.
+
+Simple Examples
+---------------
+
+To enable accelerometer::
+
+ >>> from plyer import accelerometer
+ >>> accelerometer.enable()
+
+To disable accelerometer::
+
+ >>> accelerometer.disable()
+
+To get the acceleration::
+
+ >>> accelerometer.acceleration
+ (-10.048464775085449, 6.825869083404541, 7.7260890007019043)
+
+Supported Plaforms
+------------------
+Android, iOS, OS X, Linux
+
+'''
+
+
+class Accelerometer:
+ '''
+ Accelerometer facade.
+ '''
+
+ @property
+ def acceleration(self):
+ '''
+ Property that returns values of the current acceleration
+ sensors, as a (x, y, z) tuple. Returns (None, None, None)
+ if no data is currently available.
+ '''
+ return self.get_acceleration()
+
+ def enable(self):
+ '''
+ Activate the accelerometer sensor. Throws an error if the
+ hardware is not available or not implemented on.
+ '''
+ self._enable()
+
+ def disable(self):
+ '''
+ Disable the accelerometer sensor.
+ '''
+ self._disable()
+
+ def get_acceleration(self):
+ return self._get_acceleration()
+
+ # private
+
+ def _enable(self):
+ raise NotImplementedError()
+
+ def _disable(self):
+ raise NotImplementedError()
+
+ def _get_acceleration(self):
+ raise NotImplementedError()
diff --git a/sbapp/plyer/facades/audio.py b/sbapp/plyer/facades/audio.py
new file mode 100644
index 00000000..c5e1db5f
--- /dev/null
+++ b/sbapp/plyer/facades/audio.py
@@ -0,0 +1,103 @@
+'''
+Audio
+=====
+
+The :class:`Audio` is used for recording audio.
+
+Default path for recording is set in platform implementation.
+
+.. note::
+ On Android the `RECORD_AUDIO`, `WAKE_LOCK` permissions are needed.
+
+Simple Examples
+---------------
+
+To get the file path::
+
+ >>> audio.file_path
+ '/sdcard/testrecorder.3gp'
+
+To set the file path::
+
+ >>> import os
+ >>> current_list = os.listdir('.')
+ ['/sdcard/testrecorder.3gp', '/sdcard/testrecorder1.3gp',
+ '/sdcard/testrecorder2.3gp', '/sdcard/testrecorder3.3gp']
+ >>> file_path = current_list[2]
+ >>> audio.file_path = file_path
+
+To start recording::
+
+ >>> from plyer import audio
+ >>> audio.start()
+
+To stop recording::
+
+ >>> audio.stop()
+
+To play recording::
+
+ >>> audio.play()
+
+Supported Platforms
+-------------------
+Android, Windows, macOS
+
+'''
+
+
+class Audio:
+ '''
+ Audio facade.
+ '''
+
+ state = 'ready'
+ _file_path = ''
+
+ def __init__(self, file_path=None):
+ super().__init__()
+ self._file_path = file_path or self._file_path
+
+ def start(self):
+ '''
+ Start record.
+ '''
+ self._start()
+ self.state = 'recording'
+
+ def stop(self):
+ '''
+ Stop record.
+ '''
+ self._stop()
+ self.state = 'ready'
+
+ def play(self):
+ '''
+ Play current recording.
+ '''
+ self._play()
+ self.state = 'playing'
+
+ @property
+ def file_path(self):
+ return self._file_path
+
+ @file_path.setter
+ def file_path(self, location):
+ '''
+ Location of the recording.
+ '''
+ assert isinstance(location, str), 'Location must be string or unicode'
+ self._file_path = location
+
+ # private
+
+ def _start(self):
+ raise NotImplementedError()
+
+ def _stop(self):
+ raise NotImplementedError()
+
+ def _play(self):
+ raise NotImplementedError()
diff --git a/sbapp/plyer/facades/barometer.py b/sbapp/plyer/facades/barometer.py
new file mode 100644
index 00000000..ec97b8b6
--- /dev/null
+++ b/sbapp/plyer/facades/barometer.py
@@ -0,0 +1,36 @@
+class Barometer:
+ '''Barometer facade.
+
+ Barometer sensor is used to measure the ambient air pressure in hPa.
+
+ With method `enable` you can turn on pressure sensor and 'disable'
+ method stops the sensor.
+
+ Use property `pressure` to get current air pressure in hPa.
+
+ .. versionadded:: 1.2.5
+
+ Supported Platforms:: Android, iOS
+ '''
+
+ @property
+ def pressure(self):
+ '''Current air pressure in hPa.'''
+ return self._get_pressure()
+
+ def _get_pressure(self, **kwargs):
+ raise NotImplementedError()
+
+ def _enable(self, **kwargs):
+ raise NotImplementedError()
+
+ def enable(self):
+ '''Enable barometer sensor.'''
+ self._enable()
+
+ def _disable(self, **kwargs):
+ raise NotImplementedError()
+
+ def disable(self):
+ '''Disable barometer sensor.'''
+ self._disable()
diff --git a/sbapp/plyer/facades/battery.py b/sbapp/plyer/facades/battery.py
new file mode 100644
index 00000000..f0ee147a
--- /dev/null
+++ b/sbapp/plyer/facades/battery.py
@@ -0,0 +1,54 @@
+'''
+Battery
+=======
+
+The :class:`Battery` provides information about the battery of your device.
+
+.. note::
+ On Android the `BATTERY_STATS` permission is needed.
+
+Simple Example
+---------------
+
+To get battery status::
+
+ >>> from plyer import battery
+ >>> battery.status
+ {'percentage': 82.0, 'isCharging': False}
+
+Supported Platforms
+-------------------
+Android, iOS, Windows, OS X, Linux
+
+'''
+
+
+class Battery:
+ '''
+ Battery info facade.
+ '''
+
+ @property
+ def status(self):
+ '''
+ Property that contains a dict with the following fields:
+ * **isCharging** *(bool)*: Battery is charging
+ * **percentage** *(float)*: Battery charge remaining
+
+ .. warning::
+ If any of the fields is not readable, it is set as
+ None.
+ '''
+ return self.get_state()
+
+ def get_state(self):
+ '''
+ Public method for filling battery.status via platform-specific
+ API in plyer.platforms.
+ '''
+ return self._get_state()
+
+ # private
+
+ def _get_state(self):
+ raise NotImplementedError()
diff --git a/sbapp/plyer/facades/bluetooth.py b/sbapp/plyer/facades/bluetooth.py
new file mode 100644
index 00000000..a76460d0
--- /dev/null
+++ b/sbapp/plyer/facades/bluetooth.py
@@ -0,0 +1,43 @@
+'''Bluetooth facade.
+
+Returns the following:
+
+* Bluetooth info
+
+Simple Example
+--------------
+
+To get the bluetooth status info::
+todo: will be extended to get additional bluetooth info
+todo: will be extended to allow bluetooth connections etc.
+
+ >>> from plyer import bluetooth
+ >>> bluetooth
+ 'on' or 'off'
+
+Supported Platforms
+-------------------
+Android, OS X
+
+'''
+
+
+class Bluetooth:
+ '''
+ Bluetooth facade.
+ '''
+
+ @property
+ def info(self):
+ '''
+ Property that returns the info (currently status) of the bluetooth.
+ '''
+ return self.get_info()
+
+ def get_info(self):
+ return self._get_info()
+
+ # private
+
+ def _get_info(self, **kwargs):
+ raise NotImplementedError()
diff --git a/sbapp/plyer/facades/brightness.py b/sbapp/plyer/facades/brightness.py
new file mode 100755
index 00000000..538724bd
--- /dev/null
+++ b/sbapp/plyer/facades/brightness.py
@@ -0,0 +1,58 @@
+'''
+Brightness
+==========
+
+This API helps you to control the brightness of your primary display screen.
+
+The :class:`Brightness` provides access to public methods to control the
+brightness of screen.
+
+NOTE:: For Android, make sure to add permission, WRITE_SETTINGS
+
+Simple Examples
+---------------
+To know the current brightness level of device::
+
+ >>> from plyer import brightness
+ >>> brightness.current_level()
+
+To set the brightness level to half of maximum::
+
+ >>> from plyer import brightness
+ >>> brightness.set_level(50)
+
+Supported Platforms
+-------------------
+Android, iOS, Linux
+'''
+
+
+class Brightness:
+ '''
+ Brightness facade.
+ '''
+
+ def current_level(self):
+ '''
+ Know the current level of device's brightness.
+ '''
+ return self._current_level()
+
+ def set_level(self, level):
+ '''
+ Adjust the brightness of the screen.
+ Minimum brightness level:: 1
+ Maximum brightness level:: 100
+
+ :param level: New level of brightness between 1 and 100
+ :type level: int
+ '''
+ return self._set_level(level)
+
+ # private
+
+ def _set_level(self, level):
+ raise NotImplementedError()
+
+ def _current_level(self):
+ raise NotImplementedError()
diff --git a/sbapp/plyer/facades/call.py b/sbapp/plyer/facades/call.py
new file mode 100644
index 00000000..f551b804
--- /dev/null
+++ b/sbapp/plyer/facades/call.py
@@ -0,0 +1,59 @@
+'''
+Call
+====
+
+The :class:`Call` provides access to calling feature of your device.
+
+.. note::
+ - On Android your app needs the `CALL_PHONE` or `CALL_PRIVILEGED`
+ permission in order to make calls.
+
+ - Dialing call feature in not supported yet in iOS devices.
+
+Simple Examples
+---------------
+
+To make call::
+
+ >>> from plyer import call
+ >>> tel = 9999222299
+ >>> call.makecall(tel=tel)
+
+To dial call::
+
+ >>> call.dialcall()
+
+Supported Platforms
+-------------------
+Android, iOS
+
+'''
+
+
+class Call:
+ '''
+ Call facade.
+ '''
+
+ def makecall(self, tel):
+ '''
+ Make calls using your device.
+
+ :param tel: The reciever
+ :type tel: number
+ '''
+ self._makecall(tel=tel)
+
+ def dialcall(self):
+ '''
+ Opens dialing interface.
+ '''
+ self._dialcall()
+
+ # private
+
+ def _makecall(self, **kwargs):
+ raise NotImplementedError()
+
+ def _dialcall(self, **kwargs):
+ raise NotImplementedError()
diff --git a/sbapp/plyer/facades/camera.py b/sbapp/plyer/facades/camera.py
new file mode 100644
index 00000000..ebb2615a
--- /dev/null
+++ b/sbapp/plyer/facades/camera.py
@@ -0,0 +1,88 @@
+'''
+Camera
+======
+
+The :class:`Camera` is to capture pictures and make videos.
+
+.. note::
+ - On Android the `CAMERA` , `WRITE_EXTERNAL_STORAGE`,
+ `READ_EXTERNAL_STORAGE` permissions are needed.
+
+Simple Examples
+---------------
+
+Setup callback function.
+
+ >>> from os.path import exists, join
+ >>> from plyer import camera
+ >>> def camera_callback(filepath):
+ >>> if(exists(filepath)):
+ >>> print "saved"
+ >>> else:
+ >>> print "unable to save."
+ >>> filepath = 'path/to/your/file'
+ >>> # e.g: filepath = join(App.get_running_app().user_data_dir, file_name)
+
+To take picture::
+
+ >>> file_name = "test.jpg"
+ >>> camera.take_picture(filename=file_name,
+ >>> on_complete=camera_callback)
+
+Ta take a video::
+
+ >>> file_name = "test.mp4"
+ >>> camera.take_video(filename=file_name,
+ >>> on_complete=camera_callback)
+
+Supported Platforms
+-------------------
+Android, iOS
+
+'''
+
+
+class Camera:
+ '''
+ Camera facade.
+ '''
+
+ def take_picture(self, filename, on_complete):
+ '''Ask the OS to capture a picture, and store it at filename.
+
+ When the capture is done, on_complete will be called with the filename
+ as an argument. If the callback returns True, the filename will be
+ unlinked.
+
+ :param filename: Name of the image file
+ :param on_complete: Callback that will be called when the operation is
+ done
+
+ :type filename: str
+ :type on_complete: callable
+ '''
+ self._take_picture(filename=filename, on_complete=on_complete)
+
+ def take_video(self, filename, on_complete):
+ '''Ask the OS to capture a video, and store it at filename.
+
+ When the capture is done, on_complete will be called with the filename
+ as an argument. If the callback returns True, the filename will be
+ unlinked.
+
+ :param filename: Name of the video file
+ :param on_complete: Callback that will be called when the operation is
+ done
+
+ :type filename: str
+ :type on_complete: callable
+ '''
+ self._take_video(filename=filename, on_complete=on_complete)
+
+ # private
+
+ def _take_picture(self, **kwargs):
+ raise NotImplementedError()
+
+ def _take_video(self, **kwargs):
+ raise NotImplementedError()
diff --git a/sbapp/plyer/facades/compass.py b/sbapp/plyer/facades/compass.py
new file mode 100644
index 00000000..864c58b6
--- /dev/null
+++ b/sbapp/plyer/facades/compass.py
@@ -0,0 +1,113 @@
+'''
+Compass
+=======
+
+The :class:`Compass` provides access to public methods to use compass of your
+device.
+
+Simple Examples
+---------------
+
+To enable compass::
+
+ >>> from plyer import compass
+ >>> compass.enable()
+
+To disable compass::
+
+ >>> compass.disable()
+
+To get the field::
+
+ >>> compass.field()
+ (-23.721826553344727, -5.7114701271057129, -36.749668121337891)
+
+To get the uncalibrated field along with iron bias estimation::
+
+ >>> compass.field_uncalib()
+ (a,b,c,x,y,z)
+ # a,b,c denote the Geomagnetic field strength
+ # (without hard iron calibration) along the three axes.
+ # x,y,z denote the Iron bias estimation along the three axes.
+
+Supported Platforms
+-------------------
+Android, iOS
+
+'''
+
+
+class Compass:
+ '''Compass facade.
+
+ .. versionadded:: 1.2.0
+ '''
+
+ @property
+ def orientation(self):
+ '''
+ WARNING:: This property is deprecated after API level 8.
+ Use `compass.field` instead.
+
+ Property that returns values of the current compass
+ (magnetic field) sensors, as a (x, y, z) tuple.
+ Returns (None, None, None) if no data is currently available.
+ '''
+ return self.get_orientation()
+
+ @property
+ def field(self):
+ '''
+ .. versionadded:: 1.3.1
+
+ Property that returns values of the current compass
+ (magnetic field) sensors, as a (x, y, z) tuple.
+ Returns (None, None, None) if no data is currently available.
+ '''
+ return self.get_orientation()
+
+ @property
+ def field_uncalib(self):
+ '''
+ .. versionadded:: 1.3.1
+
+ Property that returns the current value of Uncalibrated Magnetic Field
+ (without hard iron calibration) along with the iron bias estimation
+ along the three axes.
+ '''
+ return self.get_field_uncalib()
+
+ def enable(self):
+ '''
+ Activate the compass sensor.
+ '''
+ self._enable()
+
+ def disable(self):
+ '''
+ Disable the compass sensor.
+ '''
+ self._disable()
+
+ def get_orientation(self):
+ return self._get_orientation()
+
+ def get_field_uncalib(self):
+ '''
+ .. versionadded:: 1.3.1
+ '''
+ return self._get_field_uncalib()
+
+ # private
+
+ def _enable(self):
+ raise NotImplementedError()
+
+ def _disable(self):
+ raise NotImplementedError()
+
+ def _get_orientation(self):
+ raise NotImplementedError()
+
+ def _get_field_uncalib(self):
+ raise NotImplementedError()
diff --git a/sbapp/plyer/facades/cpu.py b/sbapp/plyer/facades/cpu.py
new file mode 100644
index 00000000..3107560b
--- /dev/null
+++ b/sbapp/plyer/facades/cpu.py
@@ -0,0 +1,92 @@
+'''
+CPU
+===
+
+Simple Example
+---------------
+
+To get CPU count::
+ >>> from plyer import cpu
+ >>> # 1 socket, 1 core per socket, 2 threads per core
+ >>> cpu.sockets # 1 CPU socket (or slot)
+ 1
+ >>> cpu.physical # 1 CPU socket * 1 core per socket
+ 1
+ >>> cpu.logical # 1 CPU socket * 1 core per socket * 2 threads per core
+ 2
+
+Supported Platforms
+-------------------
+
+MacOS
+Linux
+Windows
+'''
+
+
+class CPU:
+ '''
+ Facade providing info about sockets, physical and logical
+ number of processors.
+ '''
+
+ @property
+ def sockets(self):
+ '''
+ Property that contains the count of CPU sockets.
+ '''
+ return self._sockets()
+
+ @property
+ def physical(self):
+ '''
+ Property that contains the total number of physical cores
+ (max core count) in the system.
+
+ .. note:: `sockets * cores per socket`
+ '''
+ return self._physical()
+
+ @property
+ def logical(self):
+ '''
+ Property that contains the total number of logical cores
+ (max thread count) in the system.
+
+ .. note:: `sockets * cores per socket * threads per core`
+ '''
+ return self._logical()
+
+ @property
+ def cache(self):
+ '''
+ Property that contains the count of L1, L2, L3 caches in the system
+ as a dictionary `{'L1': int, 'L2': int, 'L3': int}`.
+ '''
+ return self._cache()
+
+ @property
+ def numa(self):
+ '''
+ Property that contains the count of NUMA nodes in the system.
+
+ .. note:: https://en.wikipedia.org/wiki/Non-uniform_memory_access
+ '''
+ return self._numa()
+
+ # private
+
+ def _sockets(self):
+ raise NotImplementedError()
+
+ def _physical(self):
+ raise NotImplementedError()
+
+ def _logical(self):
+ raise NotImplementedError()
+
+ def _cache(self):
+ raise NotImplementedError()
+
+ def _numa(self):
+ raise NotImplementedError()
diff --git a/sbapp/plyer/facades/devicename.py b/sbapp/plyer/facades/devicename.py
new file mode 100644
index 00000000..b58ae582
--- /dev/null
+++ b/sbapp/plyer/facades/devicename.py
@@ -0,0 +1,44 @@
+'''DeviceName facade.
+
+Returns the following depending on the platform:
+
+* **Android**: Android Device name
+* **Linux**: Hostname of the machine
+* **OS X**: Hostname of the machine
+* **Windows**: Hostname of the machine
+
+Simple Example
+--------------
+
+To get the Device Name::
+
+ >>> from plyer import devicename
+ >>> devicename.device_name
+ 'Oneplus 3'
+
+.. versionadded:: 2.1.0
+ - first release
+
+
+Supported Platforms
+-------------------
+Android, Windows, OS X, Linux
+
+'''
+
+
+class DeviceName:
+ '''
+ DeviceName facade.
+ '''
+
+ @property
+ def device_name(self):
+ '''
+ Property that returns the device name of the platform.
+ '''
+ return self._get_device_name()
+
+ # private
+ def _get_device_name(self):
+ raise NotImplementedError()
diff --git a/sbapp/plyer/facades/email.py b/sbapp/plyer/facades/email.py
new file mode 100644
index 00000000..c3360465
--- /dev/null
+++ b/sbapp/plyer/facades/email.py
@@ -0,0 +1,58 @@
+'''
+Email
+=====
+
+The :class:`Email` provides access to public methods to use email of your
+device.
+
+.. note::
+ On Android `INTERNET` permission is needed.
+
+Simple Examples
+---------------
+
+To send an e-mail::
+
+ >>> from plyer import email
+ >>> recipient = 'abc@gmail.com'
+ >>> subject = 'Hi'
+ >>> text = 'This is an example.'
+ >>> create_chooser = False
+ >>> email.send(recipient=recipient, subject=subject, text=text,
+ create_chooser=create_chooser)
+
+ >>> # opens email interface where user can change the content.
+
+Supported Platforms
+-------------------
+Android, iOS, Windows, OS X, Linux
+
+'''
+
+
+class Email:
+ '''
+ Email facade.
+ '''
+
+ def send(self, recipient=None, subject=None, text=None,
+ create_chooser=None):
+ '''
+ Open an email client message send window, prepopulated with the
+ given arguments.
+
+ :param recipient: Recipient of the message (str)
+ :param subject: Subject of the message (str)
+ :param text: Main body of the message (str)
+ :param create_chooser: Whether to display a program chooser to
+ handle the message (bool)
+
+ .. note:: create_chooser is only supported on Android
+ '''
+ self._send(recipient=recipient, subject=subject, text=text,
+ create_chooser=create_chooser)
+
+ # private
+
+ def _send(self, **kwargs):
+ raise NotImplementedError()
diff --git a/sbapp/plyer/facades/filechooser.py b/sbapp/plyer/facades/filechooser.py
new file mode 100644
index 00000000..bfad4aeb
--- /dev/null
+++ b/sbapp/plyer/facades/filechooser.py
@@ -0,0 +1,74 @@
+'''
+Native filechooser dialog facade.
+=================================
+
+open_file, save_file and choose_dir accept a number of arguments
+listed below. They return either a list of paths (normally
+absolute), or None if no file was selected or the operation was
+canceled and no result is available.
+
+Arguments:
+ * **path** *(string or None)*: a path that will be selected
+ by default, or None
+ * **multiple** *(bool)*: True if you want the dialog to
+ allow multiple file selection. (Note: Windows doesn't
+ support multiple directory selection)
+ * **filters** *(iterable)*: either a list of wildcard patterns
+ or of sequences that contain the name of the filter and any
+ number of wildcards that will be grouped under that name
+ (e.g. [["Music", "*mp3", "*ogg", "*aac"], "*jpg", "*py"])
+ * **preview** *(bool)*: True if you want the file chooser to
+ show a preview of the selected file, if supported by the
+ back-end.
+ * **title** *(string or None)*: The title of the file chooser
+ window, or None for the default title.
+ * **icon** *(string or None)*: Path to the icon of the file
+ chooser window (where supported), or None for the back-end's
+ default.
+ * **show_hidden** *(bool)*: Force showing hidden files (currently
+ supported only on Windows)
+ * **on_selection** *(func)*: Callback for fetching the selection.
+
+Important: these methods will return only after user interaction.
+Use threads or you will stop the mainloop if your app has one.
+
+.. versionchanged:: 1.4.0
+ Added Android implementation for open_file()
+ Added ``on_selection`` kwarg for callback function
+
+Supported Plaforms
+------------------
+Android, iOS, macOS, Linux, Windows
+'''
+
+
+class FileChooser:
+ '''
+ File Chooser facade.
+ '''
+
+ def open_file(self, *args, **kwargs):
+ """
+ Open the file chooser in "open" mode.
+ """
+ return self._file_selection_dialog(mode="open", *args, **kwargs)
+
+ def save_file(self, *args, **kwargs):
+ """
+ Open the file chooser in "save" mode. Confirmation will be asked
+ when a file with the same name already exists.
+ """
+ return self._file_selection_dialog(mode="save", *args, **kwargs)
+
+ def choose_dir(self, *args, **kwargs):
+ """
+ Open the directory chooser. Note that on Windows this is very
+ limited. Consider writing your own chooser if you target that
+ platform and are planning on using unsupported features.
+ """
+ return self._file_selection_dialog(mode="dir", *args, **kwargs)
+
+ # private
+
+ def _file_selection_dialog(self, **kwargs):
+ raise NotImplementedError()
diff --git a/sbapp/plyer/facades/flash.py b/sbapp/plyer/facades/flash.py
new file mode 100644
index 00000000..ac70b2ee
--- /dev/null
+++ b/sbapp/plyer/facades/flash.py
@@ -0,0 +1,77 @@
+# coding=utf-8
+'''
+Flash
+=====
+
+The :class:`Flash` provides access to public methods to use flash of your
+device.
+
+.. note::
+ In android you need CAMERA, FLASHLIGHT permissions
+ to access flash.
+
+.. versionadded:: 1.2.5
+
+This can be used to activate the flash of your camera on
+Android and iOS.
+
+Simple Examples
+---------------
+
+To turn on flash::
+
+ >>> from plyer import flash
+ >>> flash.on()
+
+To turn off flash::
+
+ >>> flash.off()
+
+To release flash::
+
+ >>> flash.release()
+
+Supported Platforms
+-------------------
+Android, iOS
+
+'''
+
+
+class Flash:
+ """
+ Flash facade.
+ """
+
+ def on(self):
+ """
+ Activate the flash
+ """
+ self._on()
+
+ def off(self):
+ """
+ Deactiavte the flash
+ """
+ self._off()
+
+ def release(self):
+ """
+ Release any access to the Flash / Camera.
+ Call this when you're done using the Flash.
+ This will release the Camera, and stop any process.
+
+ Next call to `_on` will reactivate it.
+ """
+ self._release()
+
+ # private
+
+ def _on(self):
+ raise NotImplementedError()
+
+ def _off(self):
+ raise NotImplementedError()
+
+ def _release(self):
+ pass
diff --git a/sbapp/plyer/facades/gps.py b/sbapp/plyer/facades/gps.py
new file mode 100644
index 00000000..9fc200bf
--- /dev/null
+++ b/sbapp/plyer/facades/gps.py
@@ -0,0 +1,96 @@
+'''
+GPS
+====
+
+.. versionadded:: 1.1
+
+.. note::
+ On Android `INTERNET`, `ACCESS_FINE_LOCATION`, `ACCESS_COARSE_LOCATION`
+ permissions are needed.
+
+
+.. note::
+ On iOS `NSLocationWhenInUseUsageDescription` key is required for app to
+ display geolocation usage permission prompt. Key can be added in Xcode
+ target `info` section or in ``Resources/<YourApp>-info.plist``.
+ App background mode (`on_pause`) also must be supported.
+
+You need to set a `on_location` callback with the :meth:`configure` method.
+This callback will receive a couple of keywords / values, that might be
+different depending of their availability on the targeted platform.
+Lat and lon are always available.
+
+- lat: latitude of the last location, in degrees
+- lon: longitude of the last location, in degrees
+- speed: speed of the user, in meters/second over ground
+- bearing: bearing in degrees
+- altitude: altitude in meters above the sea level
+
+Here is an example of the usage of gps::
+
+ from plyer import gps
+
+ def print_locations(**kwargs):
+ print 'lat: {lat}, lon: {lon}'.format(**kwargs)
+
+ gps.configure(on_location=print_locations)
+ gps.start()
+ # later
+ gps.stop()
+
+Supported Platforms
+-------------------
+Android, iOS
+
+'''
+
+
+class GPS:
+ '''
+ GPS facade.
+ '''
+
+ def configure(self, on_location, on_status=None):
+ '''
+ Configure the GPS object. This method should be called before
+ :meth:`start`.
+
+ :param on_location: Function to call when receiving a new location
+ :param on_status: Function to call when a status message is received
+ :type on_location: callable, multiples keys/value will be passed.
+ :type on_status: callable, args are "message-type", "status"
+
+ .. warning::
+
+ The `on_location` and `on_status` callables might be called from
+ another thread than the thread used for creating the GPS object.
+ '''
+ self.on_location = on_location
+ self.on_status = on_status
+ self._configure()
+
+ def start(self, minTime=1000, minDistance=1):
+ '''
+ Start the GPS location updates.
+ Expects 2 parameters:
+ minTime: milliseconds. (float)
+ minDistance: meters. (float)
+ '''
+ self._start(minTime=minTime, minDistance=minDistance)
+
+ def stop(self):
+ '''
+ Stop the GPS location updates.
+ '''
+ self._stop()
+
+ # private
+
+ def _configure(self):
+ raise NotImplementedError()
+
+ def _start(self, **kwargs):
+ raise NotImplementedError()
+
+ def _stop(self):
+ raise NotImplementedError()
diff --git a/sbapp/plyer/facades/gravity.py b/sbapp/plyer/facades/gravity.py
new file mode 100644
index 00000000..d6e2695f
--- /dev/null
+++ b/sbapp/plyer/facades/gravity.py
@@ -0,0 +1,38 @@
+class Gravity:
+ '''Gravity facade.
+
+ .. versionadded:: 1.2.5
+
+ Supported Platforms:: Android
+
+ '''
+
+ @property
+ def gravity(self):
+ '''Property that returns values of the current gravity force
+ as a (x, y, z) tuple. Returns (None, None, None)
+ if no data is currently available.
+ '''
+ return self._get_gravity()
+
+ def enable(self):
+ '''Activate the gravity sensor. Throws an error if the
+ hardware is not available or not implemented on.
+ '''
+ self._enable()
+
+ def disable(self):
+ '''Disable the gravity sensor.
+ '''
+ self._disable()
+
+ # private
+
+ def _enable(self):
+ raise NotImplementedError()
+
+ def _disable(self):
+ raise NotImplementedError()
+
+ def _get_gravity(self):
+ raise NotImplementedError()
diff --git a/sbapp/plyer/facades/gyroscope.py b/sbapp/plyer/facades/gyroscope.py
new file mode 100644
index 00000000..695f168a
--- /dev/null
+++ b/sbapp/plyer/facades/gyroscope.py
@@ -0,0 +1,126 @@
+'''
+Gyroscope
+============
+
+The gyroscope measures the rate of rotation around a device's x, y,
+and z axis.
+
+The :class:`Gyroscope` provides access to public methods to
+use gyroscope of your device.
+
+Simple Examples
+---------------
+
+To enable gyroscope::
+
+ >>> from plyer import gyroscope
+ >>> gyroscope.enable()
+
+To disable gyroscope::
+
+ >>> gyroscope.disable()
+
+To get the rate of rotation along the three axes::
+
+ >>> gyroscope.rotation
+ (-0.0034587313421070576, -0.0073830625042319298, 0.0046892408281564713)
+
+To get the uncalibrated rate of rotation along the three axes along with the
+drift compensation::
+
+ >>> gyroscope.rotation_uncalib
+ ()
+ where the first three values show the rate of rotation w/o drift
+ compensation and the last three show the estimated drift along the three
+ axes.
+
+Supported Platforms
+-------------------
+Android, iOS
+
+'''
+
+
+class Gyroscope:
+ '''
+ Gyroscope facade.
+
+ .. versionadded:: 1.3.1
+ '''
+
+ @property
+ def rotation(self):
+ '''
+ Property that returns the rate of rotation around the device's local
+ X, Y and Z axis.
+
+ Along x-axis: angular speed around the X axis
+ Along y-axis: angular speed around the Y axis
+ Along z-axis: angular speed around the Z axis
+
+ Returns (None, None, None) if no data is currently available.
+ '''
+ return self.get_orientation()
+
+ @property
+ def rotation_uncalib(self):
+ '''
+ Property that returns the current rate of rotation around the X, Y and
+ Z axis. An estimation of the drift on each axis is reported as well.
+
+ Along x-axis: angular speed (w/o drift compensation) around the X axis
+ Along y-axis: angular speed (w/o drift compensation) around the Y axis
+ Along z-axis: angular speed (w/o drift compensation) around the Z axis
+
+ Along x-axis: estimated drift around X axis
+ Along y-axis: estimated drift around Y axis
+ Along z-axis: estimated drift around Z axis
+
+ Returns (None, None, None, None, None, None) if no data is currently
+ available.
+ '''
+ return self.get_rotation_uncalib()
+
+ @property
+ def orientation(self):
+ '''
+ WARNING:: This property is deprecated after API Level 8.
+ Use `gyroscope.rotation` instead.
+
+ Property that returns values of the current Gyroscope sensors, as
+ a (x, y, z) tuple. Returns (None, None, None) if no data is currently
+ available.
+ '''
+ return self.get_orientation()
+
+ def enable(self):
+ '''
+ Activate the Gyroscope sensor.
+ '''
+ self._enable()
+
+ def disable(self):
+ '''
+ Disable the Gyroscope sensor.
+ '''
+ self._disable()
+
+ def get_orientation(self):
+ return self._get_orientation()
+
+ def get_rotation_uncalib(self):
+ return self._get_rotation_uncalib()
+
+ # private
+
+ def _enable(self):
+ raise NotImplementedError()
+
+ def _disable(self):
+ raise NotImplementedError()
+
+ def _get_orientation(self):
+ raise NotImplementedError()
+
+ def _get_rotation_uncalib(self):
+ raise NotImplementedError()
diff --git a/sbapp/plyer/facades/humidity.py b/sbapp/plyer/facades/humidity.py
new file mode 100644
index 00000000..ac633330
--- /dev/null
+++ b/sbapp/plyer/facades/humidity.py
@@ -0,0 +1,34 @@
+class Humidity:
+ '''Humidity facade.
+ Humidity sensor returns value of humidity.
+ With method `enable` you can turn on Humidity sensor and
+ 'disable' method stops the sensor.
+ Use property `tell` to get humidity value.
+
+ Supported Platforms
+ -------------------
+ Android
+ '''
+
+ @property
+ def tell(self):
+ '''Current humidity'''
+ return self._get_humidity()
+
+ def enable(self):
+ '''Enable Humidity sensor.'''
+ self._enable()
+
+ def disable(self):
+ '''Disable Humidity sensor.'''
+ self._disable()
+
+ # private
+ def _get_humidity(self, **kwargs):
+ raise NotImplementedError()
+
+ def _enable(self, **kwargs):
+ raise NotImplementedError()
+
+ def _disable(self, **kwargs):
+ raise NotImplementedError()
diff --git a/sbapp/plyer/facades/irblaster.py b/sbapp/plyer/facades/irblaster.py
new file mode 100644
index 00000000..a6263cde
--- /dev/null
+++ b/sbapp/plyer/facades/irblaster.py
@@ -0,0 +1,104 @@
+'''
+IrBlaster
+============
+
+The :class:`IrBlaster` provides access to public methods by which your device
+can act as a remote and could be used to control your TV, AC, Music Player,
+Projectors, Set top box or anything that can be controlled by a remote.
+
+.. note::
+ - On Android your app needs the TRANSMIT_IR permission which allows an
+ application to use the device's IR transmitter, If available.
+
+Simple Examples
+---------------
+
+To get transmit an IR sequence::
+
+ >>> from plyer import irblaster
+ >>> irblaster.transmit(frequency, pattern, mode)
+
+To get frequencies::
+
+ >>> irblaster.frequencies
+
+To check if IrBlaster exists::
+
+ >>> irblaster.exists()
+ True/False
+
+Supported Platforms
+-------------------
+Android
+
+'''
+
+
+class IrBlaster:
+ '''
+ Infrared blaster facade.
+ '''
+
+ @staticmethod
+ def periods_to_microseconds(frequency, pattern):
+ '''
+ Convert a pattern from period counts to microseconds.
+ '''
+ period = 1000000. / frequency
+ return [period * x for x in pattern]
+
+ @staticmethod
+ def microseconds_to_periods(frequency, pattern):
+ '''
+ Convert a pattern from microseconds to period counts.
+ '''
+ period = 1000000. / frequency
+ return [x / period for x in pattern]
+
+ @property
+ def frequencies(self):
+ '''
+ Property which contains a list of frequency ranges
+ supported by the device in the form:
+
+ [(from1, to1),
+ (from2, to2),
+ ...
+ (fromN, toN)]
+ '''
+ return self.get_frequencies()
+
+ def get_frequencies(self):
+ return self._get_frequencies()
+
+ def transmit(self, frequency, pattern, mode='period'):
+ '''
+ Transmit an IR sequence.
+
+ :parameters:
+ `frequency`: int
+ Carrier frequency for the IR transmission.
+ `pattern`: list[int]
+ Burst pair pattern to transmit.
+ `mode`: str, defaults to 'period'
+ Specifies the format of the pattern values.
+ Can be 'period' or 'microseconds'.
+ '''
+ return self._transmit(frequency, pattern, mode)
+
+ def exists(self):
+ '''
+ Check if the device has an infrared emitter.
+ '''
+ return self._exists()
+
+ # private
+
+ def _get_frequencies(self):
+ raise NotImplementedError()
+
+ def _transmit(self, frequency, pattern, mode):
+ raise NotImplementedError()
+
+ def _exists(self):
+ raise NotImplementedError()
diff --git a/sbapp/plyer/facades/keystore.py b/sbapp/plyer/facades/keystore.py
new file mode 100644
index 00000000..5d0f21d2
--- /dev/null
+++ b/sbapp/plyer/facades/keystore.py
@@ -0,0 +1,32 @@
+'''
+Keystore
+=======
+The :class:`Keystore` provides a mechanism for securing/storing
+cryptographic keys (such as user credentials) in a container.
+Typically needed to support authentication APIs such as OAuth2
+.. note::
+ Typically needed to support authentication APIs such as OAuth2
+
+Supported Platforms
+-------------------
+Android, iOS, Windows, OS X, Linux
+---------------
+'''
+
+
+class Keystore:
+ '''
+ Keystore facade
+ '''
+
+ def set_key(self, servicename, key, value, **kwargs):
+ self._set_key(servicename, key, value, **kwargs)
+
+ def _set_key(self, servicename, key, value, **kwargs):
+ raise NotImplementedError()
+
+ def get_key(self, servicename, key, **kwargs):
+ return self._get_key(servicename, key)
+
+ def _get_key(self, servicename, key, **kwargs):
+ raise NotImplementedError()
diff --git a/sbapp/plyer/facades/light.py b/sbapp/plyer/facades/light.py
new file mode 100644
index 00000000..498e28ed
--- /dev/null
+++ b/sbapp/plyer/facades/light.py
@@ -0,0 +1,39 @@
+class Light:
+ '''Light facade.
+
+ Light sensor measures the ambient light level(illumination) in lx.
+ Common uses include controlling screen brightness.
+
+ With method `enable` you can turn on the sensor and
+ `disable` method stops the sensor.
+
+ Use property `illumination` to get current illumination in lx.
+
+ .. versionadded:: 1.2.5
+
+ Supported Platforms:: Android
+ '''
+
+ @property
+ def illumination(self):
+ '''Current illumination in lx.'''
+ return self._get_illumination()
+
+ def enable(self):
+ '''Enable light sensor.'''
+ self._enable()
+
+ def disable(self):
+ '''Disable light sensor.'''
+ self._disable()
+
+ # private
+
+ def _get_illumination(self, **kwargs):
+ raise NotImplementedError()
+
+ def _enable(self, **kwargs):
+ raise NotImplementedError()
+
+ def _disable(self, **kwargs):
+ raise NotImplementedError()
diff --git a/sbapp/plyer/facades/notification.py b/sbapp/plyer/facades/notification.py
new file mode 100644
index 00000000..5b09dee1
--- /dev/null
+++ b/sbapp/plyer/facades/notification.py
@@ -0,0 +1,93 @@
+'''
+Notification
+============
+
+The :class:`Notification` provides access to public methods to create
+notifications.
+
+Simple Examples
+---------------
+
+To send notification::
+
+ >>> from plyer import notification
+ >>> title = 'plyer'
+ >>> message = 'This is an example.'
+ >>> notification.notify(title=title, message=message)
+
+Android toast notification::
+
+ >>> from plyer import notification
+ >>> notification.notify(message='hello', toast=True)
+
+Android simple notification::
+
+ >>> from plyer import notification
+ >>> notification.notify(message='hello', toast=True)
+
+Notification with custom icon::
+
+ >>> from plyer import notification
+ >>> notification.notify(title='title', message='hello', app_icon=<path>)
+
+.. versionadded:: 1.0.0
+
+.. versionadded:: 1.4.0
+ Add implementation of primitive Android popup-like notification (toast)
+
+.. versionchanged:: 1.4.0
+ Android implementation now supports custom icons for notifications.
+'''
+
+
+class Notification:
+ '''
+ Notification facade.
+ '''
+
+ def notify(self, title='', message='', app_name='', app_icon='', notification_icon=None,
+ timeout=10, ticker='', toast=False, hints={}):
+ '''
+ Send a notification.
+
+ :param title: Title of the notification
+ :param message: Message of the notification
+ :param app_name: Name of the app launching this notification
+ :param app_icon: Icon to be displayed along with the message
+ :param timeout: time to display the message for, defaults to 10
+ :param ticker: text to display on status bar as the notification
+ arrives
+ :param toast: simple Android message instead of full notification
+ :param hints: Optional hints that can be used to pass along extra
+ instructions on Linux.
+ (See https://specifications.freedesktop.org/notification-spec/latest/ar01s08.html) # noqa: E501
+
+ :type title: str
+ :type message: str
+ :type app_name: str
+ :type app_icon: str
+ :type timeout: int
+ :type ticker: str
+ :type toast: bool
+ :type hints: dict
+
+ .. note::
+ When called on Windows, ``app_icon`` has to be a path to
+ a file in .ICO format.
+
+ .. versionadded:: 1.0.0
+
+ .. versionchanged:: 1.4.0
+ Add 'toast' keyword argument
+ '''
+
+ self._notify(
+ title=title, message=message,
+ app_icon=app_icon, app_name=app_name, notification_icon=notification_icon,
+ timeout=timeout, ticker=ticker, toast=toast, hints=hints
+ )
+
+ # private
+
+ def _notify(self, **kwargs):
+ raise NotImplementedError("No usable implementation found!")
diff --git a/sbapp/plyer/facades/orientation.py b/sbapp/plyer/facades/orientation.py
new file mode 100644
index 00000000..56ea0bad
--- /dev/null
+++ b/sbapp/plyer/facades/orientation.py
@@ -0,0 +1,81 @@
+'''
+Orientation
+==========
+
+The :class:`Orientation` provides access to public methods to set orientation
+of your device.
+
+.. note::
+ These settings are generally guidelines, the operating
+ system may choose to ignore them, or they may be overridden by
+ other system components.
+
+.. versionadded:: 1.2.4
+
+Simple Examples
+---------------
+
+To set landscape::
+
+ >>> from plyer import orientation
+ >>> orientation.set_landscape()
+
+To set portrait::
+
+ >>> orientation.set_portrait()
+
+To set sensor::
+
+ >>> orientation.set_sensor()
+
+Supported Platforms
+-------------------
+Android
+
+'''
+
+
+class Orientation:
+ '''
+ Orientation facade.
+ '''
+
+ def set_landscape(self, reverse=False):
+ '''
+ Rotate the app to a landscape orientation.
+
+ :param reverse: If True, uses the opposite of the natural
+ orientation.
+ '''
+ self._set_landscape(reverse=reverse)
+
+ def set_portrait(self, reverse=False):
+ '''
+ Rotate the app to a portrait orientation.
+
+ :param reverse: If True, uses the opposite of the natural
+ orientation.
+ '''
+ self._set_portrait(reverse=reverse)
+
+ def set_sensor(self, mode='any'):
+ '''
+ Rotate freely following sensor information from the device.
+
+ :param mode: The rotation mode, should be one of 'any' (rotate
+ to any orientation), 'landscape' (choose nearest
+ landscape mode) or 'portrait' (choose nearest
+ portrait mode). Defaults to 'any'.
+ '''
+ self._set_sensor(mode=mode)
+
+ # private
+
+ def _set_landscape(self, **kwargs):
+ raise NotImplementedError()
+
+ def _set_portrait(self, **kwargs):
+ raise NotImplementedError()
+
+ def _set_sensor(self, **kwargs):
+ raise NotImplementedError()
diff --git a/sbapp/plyer/facades/processors.py b/sbapp/plyer/facades/processors.py
new file mode 100644
index 00000000..04b7440d
--- /dev/null
+++ b/sbapp/plyer/facades/processors.py
@@ -0,0 +1,42 @@
+'''
+Number of Processors
+=======
+The :class:`Processors` provides a information on the number of
+processors in a system
+.. note::
+ Deprecated in favor of `cpu`
+
+Simple Example
+---------------
+To get processors status::
+ >>> from plyer import processors
+ >>> processors.status
+ {'Number_of_Processors': '4'}
+Supported Platforms
+-------------------
+Linux
+'''
+
+
+class Processors:
+ '''
+ Number of Processors info facade.
+ '''
+
+ @property
+ def status(self):
+ '''
+ Property that contains a dict with the following fields:
+ * **Number_of_Processors** *(int)*: Number of Processors in
+ the system
+ .. warning::
+ If any of the fields is not readable, it is set as
+ None.
+ '''
+ return self.get_state()
+
+ def get_state(self):
+ return self._get_state()
+
+ def _get_state(self):
+ raise NotImplementedError()
diff --git a/sbapp/plyer/facades/proximity.py b/sbapp/plyer/facades/proximity.py
new file mode 100644
index 00000000..d0fc303a
--- /dev/null
+++ b/sbapp/plyer/facades/proximity.py
@@ -0,0 +1,44 @@
+class Proximity:
+ '''Proximity facade.
+
+ The proximity sensor is commonly used to determine distance whether
+ phone is close to your head. Commonly is used when you have a call
+ and you stick your phone with your head. Then screen of phone turns off.
+
+ Use method `enable` to turn on proximity sensor and method `disable` for
+ turn off.
+
+ To check if some object (or your head) is near sensor check values from
+ property `proximity`. It returns `True` when object is close.
+
+ .. versionadded:: 1.2.5
+
+ Supported Platforms::Android
+ '''
+
+ @property
+ def proximity(self):
+ '''Return True or False depending if there is an object or not.
+
+ :return: True if there is an object. Otherwise False.
+ '''
+ return self._get_proximity()
+
+ def _enable(self, **kwargs):
+ raise NotImplementedError()
+
+ def enable(self):
+ '''Enable the proximity sensor.
+ '''
+ self._enable()
+
+ def _disable(self, **kwargs):
+ raise NotImplementedError()
+
+ def disable(self):
+ '''Disable the proximity sensor.
+ '''
+ self._disable()
+
+ def _get_proximity(self):
+ raise NotImplementedError()
diff --git a/sbapp/plyer/facades/screenshot.py b/sbapp/plyer/facades/screenshot.py
new file mode 100644
index 00000000..ddfb2930
--- /dev/null
+++ b/sbapp/plyer/facades/screenshot.py
@@ -0,0 +1,57 @@
+'''
+Screenshot
+==========
+
+The :class:`Screenshot` is used for capturing a digital image of what
+is currently visible on the monitor.
+
+The default path for taking screenshot is set in each platform implementation.
+
+Simple Examples
+---------------
+
+To get the file path::
+
+ >>> screenshot.file_path
+ '/sdcard/test.jpg'
+
+To set the file path::
+
+ >>> screenshot.file_path = '/Users/OSXUser/Pictures/screenshot.png'
+
+To take screenshot::
+
+ >>> from plyer import screenshot
+ >>> screenshot.capture()
+'''
+
+
+class Screenshot:
+ '''
+ Screenshot facade.
+ '''
+
+ _file_path = ''
+
+ def __init__(self, file_path=None):
+ self._file_path = file_path
+
+ def capture(self):
+ self._capture()
+
+ @property
+ def file_path(self):
+ return self._file_path
+
+ @file_path.setter
+ def file_path(self, location):
+ '''
+ Location of the screenshot.
+ '''
+
+ self._file_path = location
+
+ # private
+
+ def _capture(self, **kwargs):
+ raise NotImplementedError()
diff --git a/sbapp/plyer/facades/sms.py b/sbapp/plyer/facades/sms.py
new file mode 100644
index 00000000..0e58f44a
--- /dev/null
+++ b/sbapp/plyer/facades/sms.py
@@ -0,0 +1,51 @@
+'''
+Sms
+====
+
+The :class:`Sms` provides access to sending Sms from your device.
+
+.. note::
+
+ On Android your app needs the SEND_SMS permission in order to
+ send sms messages.
+
+.. versionadded:: 1.2.0
+
+Simple Examples
+---------------
+
+To send sms::
+
+ >>> from plyer import sms
+ >>> recipient = 9999222299
+ >>> message = 'This is an example.'
+ >>> sms.send(recipient=recipient, message=message)
+
+Supported Platforms
+-------------------
+Android, iOS
+
+'''
+
+
+class Sms:
+ '''
+ Sms facade.
+ '''
+
+ def send(self, recipient, message):
+ '''
+ Send SMS or open SMS interface.
+
+ :param recipient: The receiver
+ :param message: the message
+
+ :type recipient: number
+ :type message: str
+ '''
+ self._send(recipient=recipient, message=message)
+
+ # private
+
+ def _send(self, **kwargs):
+ raise NotImplementedError()
diff --git a/sbapp/plyer/facades/spatialorientation.py b/sbapp/plyer/facades/spatialorientation.py
new file mode 100644
index 00000000..9fe400b2
--- /dev/null
+++ b/sbapp/plyer/facades/spatialorientation.py
@@ -0,0 +1,54 @@
+# coding=utf-8
+
+
+class SpatialOrientation:
+ '''Spatial Orientation facade.
+
+ Computes the device's orientation based on the rotation matrix.
+
+ .. versionadded:: 1.3.1
+ '''
+
+ @property
+ def orientation(self):
+ '''Property that returns values of the current device orientation
+ as a (azimuth, pitch, roll) tuple.
+
+ Azimuth, angle of rotation about the -z axis. This value represents the
+ angle between the device's y axis and the magnetic north pole.
+ The range of values is -π to π.
+
+ Pitch, angle of rotation about the x axis. This value represents the
+ angle between a plane parallel to the device's screen and a plane
+ parallel to the ground.
+ The range of values is -π to π.
+
+ Roll, angle of rotation about the y axis. This value represents the
+ angle between a plane perpendicular to the device's screen and a plane
+ perpendicular to the ground.
+ The range of values is -π/2 to π/2.
+
+ Returns (None, None, None) if no data is currently available.
+
+ Supported Platforms:: Android
+ '''
+ return self._get_orientation() or (None, None, None)
+
+ def _get_orientation(self):
+ raise NotImplementedError()
+
+ def enable_listener(self):
+ '''Enable the orientation sensor.
+ '''
+ self._enable_listener()
+
+ def _enable_listener(self, **kwargs):
+ raise NotImplementedError()
+
+ def disable_listener(self):
+ '''Disable the orientation sensor.
+ '''
+ self._disable_listener()
+
+ def _disable_listener(self, **kwargs):
+ raise NotImplementedError()
diff --git a/sbapp/plyer/facades/storagepath.py b/sbapp/plyer/facades/storagepath.py
new file mode 100644
index 00000000..7445a563
--- /dev/null
+++ b/sbapp/plyer/facades/storagepath.py
@@ -0,0 +1,135 @@
+'''
+Storage Path
+============
+
+The StorgePath API can be used to gain access to standard storage locations
+across platforms such as home directory, root directory, external storage
+directory, documents, downloads, etc.
+
+The :class:`StoragePath` provides access to public methods to access standard
+storage locations.
+
+Simple Examples
+---------------
+
+To get the path of user's home directory::
+
+ >>> from plyer import storagepath
+ >>> storagepath.get_home_dir()
+
+To get the path of standard downloads directory::
+
+ >>> from plyer import storagepath
+ >>> storagepath.get_downloads_dir()
+
+To get the path of directory holding application files::
+
+ >>> from plyer import storagepath
+ >>> storagepath.get_application_dir()
+
+'''
+
+
+class StoragePath:
+ '''
+ StoragePath facade.
+ '''
+
+ def get_home_dir(self):
+ '''
+ Get the path of home directory of current user.
+ '''
+ return self._get_home_dir()
+
+ def get_external_storage_dir(self):
+ '''
+ Get the path of primary shared or external storage directory.
+ '''
+ return self._get_external_storage_dir()
+
+ def get_sdcard_dir(self):
+ '''
+ Get the path of external SD card.
+
+ .. versionadded:: 1.4.0
+ '''
+ return self._get_sdcard_dir()
+
+ def get_root_dir(self):
+ '''
+ Get the path of root of the "system" partition holding the core OS.
+ '''
+ return self._get_root_dir()
+
+ def get_documents_dir(self):
+ '''
+ Get the path of standard directory in which to place documents that
+ have been created by the user.
+ '''
+ return self._get_documents_dir()
+
+ def get_downloads_dir(self):
+ '''
+ Get the path of standard directory in which to place files that have
+ been downloaded by the user.
+ '''
+ return self._get_downloads_dir()
+
+ def get_videos_dir(self):
+ '''
+ Get the path of standard directory in which to place videos that are
+ available to the user.
+ '''
+ return self._get_videos_dir()
+
+ def get_music_dir(self):
+ '''
+ Get the path of standard directory in which to place any audio files
+ that should be in the regular list of music for the user.
+ '''
+ return self._get_music_dir()
+
+ def get_pictures_dir(self):
+ '''
+ Standard directory in which to place pictures that are available to
+ the user.
+ '''
+ return self._get_pictures_dir()
+
+ def get_application_dir(self):
+ '''
+ Get the path of the directory holding application files.
+ '''
+ return self._get_application_dir()
+
+ # private
+
+ def _get_home_dir(self):
+ raise NotImplementedError()
+
+ def _get_external_storage_dir(self):
+ raise NotImplementedError()
+
+ def _get_sdcard_dir(self):
+ raise NotImplementedError()
+
+ def _get_root_dir(self):
+ raise NotImplementedError()
+
+ def _get_documents_dir(self):
+ raise NotImplementedError()
+
+ def _get_downloads_dir(self):
+ raise NotImplementedError()
+
+ def _get_videos_dir(self):
+ raise NotImplementedError()
+
+ def _get_music_dir(self):
+ raise NotImplementedError()
+
+ def _get_pictures_dir(self):
+ raise NotImplementedError()
+
+ def _get_application_dir(self):
+ raise NotImplementedError()
diff --git a/sbapp/plyer/facades/stt.py b/sbapp/plyer/facades/stt.py
new file mode 100644
index 00000000..9306df61
--- /dev/null
+++ b/sbapp/plyer/facades/stt.py
@@ -0,0 +1,188 @@
+'''
+Speech to Text
+==============
+
+.. versionadded:: 1.4.0
+
+Speech Recognition facade.
+
+In order to check that your device supports voice recognition use method
+`exist`.
+
+Variable `language` indicates which language will be used to match words from
+ voice.
+
+Use `start` to start voice recognition immediately and `stop` to stop.
+
+.. note::
+ Needed permissions for Android: `RECORD_AUDIO` (and `INTERNET` if you want
+ online voice recognition API to be used)
+
+.. note::
+ On Android platform, after execute `start` method you can hear BEEP!
+ Mute sound in order to disable it.
+
+.. note::
+ For Android implementation to work there has to be an application with
+ `android.speech.RecognitionService` implementation present in the system.
+ Mostly it's `com.google.android.googlequicksearchbox` or "Google"
+ application (the search bar with the launcher widget).
+
+Offline Speech Recognition on Android
+-------------------------------------
+
+Requires any application that provides an
+`android.speech.RecognitionService` implementation to the other apps. One of
+such applications is on a lot of devices preinstalled Google (quick search
+box).
+
+The API prefers offline recognition, but should be able to switch to online
+alternative in case you don't have a language package installed (`INTERNET`
+permission necessary).
+
+You can enable offline speech recognition this way (Android 8.1):
+
+* open the `Settings` app
+* choose `Language & Input` / `Language & Keyboard` (Samsung might include it
+ in the `General` category)
+* choose `On-Screen keyboard` or `Voice search`
+* choose `Google Keyboard`
+* choose `Offline Speech recognition`
+* download language package if you don't have one already
+
+Simple Examples
+---------------
+
+To start listening::
+
+ >>> from plyer import stt
+ >>> stt.start()
+
+To retrieve partial results while listening::
+
+ >>> assert stt.listening
+ >>> print(stt.partial_results)
+
+To stop listening::
+
+ >>> stt.stop()
+
+To retrieve results after the listening stopped::
+
+ >>> print(stt.results)
+'''
+
+
+class STT:
+ '''
+ Speech to text facade.
+ '''
+
+ _language = 'en-US'
+ '''
+ Default language in which platform will try to recognize voice.
+ In order to change language pick one from list by using
+ `supported_languages` method.
+ '''
+
+ _supported_languages = [
+ 'en-US',
+ 'pl-PL'
+ ]
+
+ results = []
+ '''
+ List of sentences found while listening. It may consist of many similar
+ and possible sentences that was recognition program.
+ '''
+
+ errors = []
+ '''
+ List of errors found while listening.
+ '''
+
+ partial_results = []
+ '''
+ List of results found while the listener is still being active.
+ '''
+
+ prefer_offline = True
+ '''
+ Preference whether to use offline language package necessary for
+ each platform dependant implementation or online API.
+ '''
+
+ listening = False
+ '''
+ Current state of listening.
+ '''
+
+ @property
+ def supported_languages(self):
+ '''
+ Return list of supported languages used in recognition.
+ '''
+
+ return self._supported_languages
+
+ @property
+ def language(self):
+ '''
+ Return current language.
+ '''
+
+ return self._language
+
+ @language.setter
+ def language(self, lang):
+ '''
+ Set current language.
+
+ Value can not be set if it's not supported. See `supported_languages`
+ to get what language you can set.
+
+ .. note::
+ We obviously can't check each language, therefore if you find
+ that a specific language is available to you and the only limitation
+ is our check for the internally defined `supported_languages`, feel
+ free to open a pull request for adding your language to the list.
+ '''
+
+ if lang in self.supported_languages:
+ self._language = lang
+
+ # public methods
+ def start(self):
+ '''
+ Start listening.
+ '''
+
+ self.results = []
+ self.partial_results = []
+ self._start()
+ self.listening = True
+
+ def stop(self):
+ '''
+ Stop listening.
+ '''
+
+ self._stop()
+ self.listening = False
+
+ def exist(self):
+ '''
+ Returns a boolean for speech recognition availability.
+ '''
+
+ return self._exist()
+
+ # private methods
+ def _start(self):
+ raise NotImplementedError
+
+ def _stop(self):
+ raise NotImplementedError
+
+ def _exist(self):
+ raise NotImplementedError
diff --git a/sbapp/plyer/facades/temperature.py b/sbapp/plyer/facades/temperature.py
new file mode 100644
index 00000000..baaae2a9
--- /dev/null
+++ b/sbapp/plyer/facades/temperature.py
@@ -0,0 +1,41 @@
+# coding=utf-8
+
+
+class Temperature:
+ '''Temperature facade.
+
+ Temperature sensor is used to measure the ambient room temperature in
+ degrees Celsius (°C)
+ With method `enable` you can turn on temperature sensor and 'disable'
+ method stops the sensor.
+ Use property `temperature` to get ambient air temperature in degree C.
+
+ .. versionadded:: 1.2.5
+
+ Supported Platforms:: Android
+
+ '''
+
+ @property
+ def temperature(self):
+ '''Current air temperature in degree C.'''
+ return self._get_temperature()
+
+ def enable(self):
+ '''Enable temperature sensor.'''
+ self._enable()
+
+ def disable(self):
+ '''Disable temperature sensor.'''
+ self._disable()
+
+ # private
+
+ def _get_temperature(self, **kwargs):
+ raise NotImplementedError()
+
+ def _enable(self, **kwargs):
+ raise NotImplementedError()
+
+ def _disable(self, **kwargs):
+ raise NotImplementedError()
diff --git a/sbapp/plyer/facades/tts.py b/sbapp/plyer/facades/tts.py
new file mode 100644
index 00000000..c4fd3668
--- /dev/null
+++ b/sbapp/plyer/facades/tts.py
@@ -0,0 +1,39 @@
+'''
+TTS
+====
+
+The :class:`TTS` provides provides access to public methods to
+use text to speech of your device.
+
+Simple Examples
+---------------
+
+To speak::
+
+ >>> from plyer import tts
+ >>> tts.speak(message=message)
+
+Supported Platforms
+-------------------
+Android, iOS, Windows, OS X, Linux
+
+'''
+
+
+class TTS:
+ '''
+ TextToSpeech facade.
+ '''
+
+ def speak(self, message=''):
+ '''Use text to speech capabilities to speak the message.
+
+ :param message: What to speak
+ :type message: str
+ '''
+ self._speak(message=message)
+
+ # private
+
+ def _speak(self, **kwargs):
+ raise NotImplementedError()
diff --git a/sbapp/plyer/facades/uniqueid.py b/sbapp/plyer/facades/uniqueid.py
new file mode 100644
index 00000000..07630e3d
--- /dev/null
+++ b/sbapp/plyer/facades/uniqueid.py
@@ -0,0 +1,54 @@
+'''UniqueID facade.
+
+Returns the following depending on the platform:
+
+* **Android**: Android ID
+* **OS X**: Serial number of the device
+* **Linux**: Serial number using lshw
+* **Windows**: MachineGUID from regkey
+* **iOS**: UUID
+
+Simple Example
+--------------
+
+To get the unique ID::
+
+ >>> from plyer import uniqueid
+ >>> uniqueid.id
+ '1b1a7a4958e2a845'
+
+.. versionadded:: 1.2.0
+
+.. versionchanged:: 1.2.4
+ On Android returns Android ID instead of IMEI.
+
+Supported Platforms
+-------------------
+Android, iOS, Windows, OS X, Linux
+
+'''
+
+
+class UniqueID:
+ '''
+ UniqueID facade.
+ '''
+
+ @property
+ def id(self):
+ '''
+ Property that returns the unique id of the platform.
+ '''
+ return self.get_uid()
+
+ def get_uid(self):
+ '''
+ Public method for receiving unique ID via platform-specific
+ API in plyer.platforms.
+ '''
+ return self._get_uid()
+
+ # private
+
+ def _get_uid(self):
+ raise NotImplementedError()
diff --git a/sbapp/plyer/facades/vibrator.py b/sbapp/plyer/facades/vibrator.py
new file mode 100644
index 00000000..345550c0
--- /dev/null
+++ b/sbapp/plyer/facades/vibrator.py
@@ -0,0 +1,97 @@
+'''
+Vibrator
+=======
+
+The :class:`Vibrator` provides access to public methods to use vibrator of your
+device.
+
+.. note::
+ On Android your app needs the VIBRATE permission to
+ access the vibrator.
+
+Simple Examples
+---------------
+
+To vibrate your device::
+
+ >>> from plyer import vibrator
+ >>> time=2
+ >>> vibrator.vibrate(time=time)
+
+To set a pattern::
+
+ >>> vibrator.pattern(pattern=pattern, repeat=repeat)
+
+To know whether vibrator exists or not::
+
+ >>> vibrator.exists()
+
+To cancel vibration::
+
+ >>> vibrator.cancel()
+
+Supported Platforms
+-------------------
+Android, iOS
+
+
+'''
+
+
+class Vibrator:
+ '''
+ Vibration facade.
+ '''
+
+ def vibrate(self, time=1):
+ '''
+ Ask the vibrator to vibrate for the given period.
+
+ :param time: Time to vibrate for, in seconds. Default is 1.
+ '''
+ self._vibrate(time=time)
+
+ def pattern(self, pattern=(0, 1), repeat=-1):
+ '''
+ Ask the vibrator to vibrate with the given pattern, with an
+ optional repeat.
+
+ :param pattern: Pattern to vibrate with. Should be a list of
+ times in seconds. The first number is how long to wait
+ before vibrating, and subsequent numbers are times to
+ vibrate and not vibrate alternately.
+ Defaults to ``[0, 1]``.
+
+ :param repeat: Index at which to repeat the pattern. When the
+ vibration pattern reaches this index, it will start again
+ from the beginning. Defaults to ``-1``, which means no
+ repeat.
+ '''
+ self._pattern(pattern=pattern, repeat=repeat)
+
+ def exists(self):
+ '''
+ Check if the device has a vibrator. Returns True or
+ False.
+ '''
+ return self._exists()
+
+ def cancel(self):
+ '''
+ Cancels any current vibration, and stops the vibrator.
+ '''
+ self._cancel()
+
+ # private
+
+ def _vibrate(self, **kwargs):
+ raise NotImplementedError()
+
+ def _pattern(self, **kwargs):
+ raise NotImplementedError()
+
+ def _exists(self, **kwargs):
+ raise NotImplementedError()
+
+ def _cancel(self, **kwargs):
+ raise NotImplementedError()
diff --git a/sbapp/plyer/facades/wifi.py b/sbapp/plyer/facades/wifi.py
new file mode 100644
index 00000000..5f0742f3
--- /dev/null
+++ b/sbapp/plyer/facades/wifi.py
@@ -0,0 +1,187 @@
+'''
+Wifi Facade.
+=============
+
+The :class:`Wifi` is to provide access to the wifi of your mobile/ desktop
+devices.
+
+It currently supports `connecting`, `disconnecting`, `scanning`, `getting
+available wifi network list` and `getting network information`.
+
+Simple examples
+---------------
+
+To enable/ turn on wifi scanning::
+
+ >>> from plyer import wifi
+ >>> wifi.start_scanning()
+
+Once the wifi is enabled/ turned on, then this command starts to scan
+all the nearby available wifi networks.
+
+To get network info::
+
+ >>> from plyer import wifi
+ >>> wifi.start_scanning()
+ >>> return wifi.get_network_info(name)
+
+Returns network details of the network who's name/ssid is provided in the
+`name` parameter.
+
+To connect to a network::
+
+ >>> from plyer import wifi
+ >>> wifi.start_scanning()
+ >>> wifi.connect(network, parameters)
+
+This connects to the network who's name/ssid is provided under `network`
+parameter and along with other necessary methods for connection
+which depends upon platform to platform.
+
+please visit following files for more details about requirements of
+`paramaters` argument in `connect` method:
+
+ plyer/platforms/win/wifi.py
+ plyer/platforms/macosx/wifi.py
+ plyer/platforms/win/wifi.py
+
+To disconnect from wifi::
+
+ >>> from plyer import wifi
+ >>> wifi.disconnect()
+
+This disconnects your device from any wifi network.
+
+To get available wifi networks::
+
+ >>> from plyer import wifi
+ >>> wifi.start_scanning()
+ >>> return wifi.get_available_wifi()
+
+This returns all the available wifi networks near the device.
+
+Supported Platforms
+-------------------
+Windows, OS X, Linux
+
+Ex: 6
+----------
+
+from plyer import wifi
+wifi.enable()
+
+This enables wifi device.
+
+Ex: 7
+----------
+
+from plyer import wifi
+wifi.disable()
+
+This disable wifi device
+'''
+
+
+class Wifi:
+ '''
+ Wifi Facade.
+ '''
+
+ def is_enabled(self):
+ '''
+ Return enabled status of WiFi hardware.
+ '''
+ return self._is_enabled()
+
+ def is_connected(self, interface=None):
+ '''
+ Return connection state of WiFi interface.
+
+ .. versionadded:: 1.4.0
+ '''
+ return self._is_connected(interface=interface)
+
+ @property
+ def interfaces(self):
+ '''
+ List all available WiFi interfaces.
+
+ .. versionadded:: 1.4.0
+ '''
+
+ raise NotImplementedError()
+
+ def start_scanning(self, interface=None):
+ '''
+ Turn on scanning.
+ '''
+ return self._start_scanning(interface=interface)
+
+ def get_network_info(self, name):
+ '''
+ Return a dictionary of specified network.
+ '''
+ return self._get_network_info(name=name)
+
+ def get_available_wifi(self):
+ '''
+ Returns a list of all the available wifi.
+ '''
+ return self._get_available_wifi()
+
+ def connect(self, network, parameters, interface=None):
+ '''
+ Method to connect to some network.
+ '''
+ self._connect(
+ network=network,
+ parameters=parameters,
+ interface=interface
+ )
+
+ def disconnect(self, interface=None):
+ '''
+ To disconnect from some network.
+ '''
+ self._disconnect(interface=interface)
+
+ def enable(self):
+ '''
+ Wifi interface power state is set to "ON".
+ '''
+ self._enable()
+
+ def disable(self):
+ '''
+ Wifi interface power state is set to "OFF".
+ '''
+ self._disable()
+
+ # private
+
+ def _is_enabled(self):
+ raise NotImplementedError()
+
+ def _is_connected(self, interface=None):
+ raise NotImplementedError()
+
+ def _start_scanning(self, interface=None):
+ raise NotImplementedError()
+
+ def _get_network_info(self, **kwargs):
+ raise NotImplementedError()
+
+ def _get_available_wifi(self):
+ raise NotImplementedError()
+
+ def _connect(self, **kwargs):
+ raise NotImplementedError()
+
+ def _disconnect(self, interface=None):
+ raise NotImplementedError()
+
+ def _enable(self):
+ raise NotImplementedError()
+
+ def _disable(self):
+ raise NotImplementedError()
diff --git a/sbapp/plyer/platforms/__init__.py b/sbapp/plyer/platforms/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/sbapp/plyer/platforms/android/__init__.py b/sbapp/plyer/platforms/android/__init__.py
new file mode 100644
index 00000000..8fe12635
--- /dev/null
+++ b/sbapp/plyer/platforms/android/__init__.py
@@ -0,0 +1,18 @@
+from os import environ
+from jnius import autoclass
+
+ANDROID_VERSION = autoclass('android.os.Build$VERSION')
+SDK_INT = ANDROID_VERSION.SDK_INT
+
+try:
+ from android import config
+ ns = config.JAVA_NAMESPACE
+except (ImportError, AttributeError):
+ ns = 'org.renpy.android'
+
+if 'PYTHON_SERVICE_ARGUMENT' in environ:
+ PythonService = autoclass(ns + '.PythonService')
+ activity = PythonService.mService
+else:
+ PythonActivity = autoclass(ns + '.PythonActivity')
+ activity = PythonActivity.mActivity
diff --git a/sbapp/plyer/platforms/android/accelerometer.py b/sbapp/plyer/platforms/android/accelerometer.py
new file mode 100644
index 00000000..00c73cfa
--- /dev/null
+++ b/sbapp/plyer/platforms/android/accelerometer.py
@@ -0,0 +1,79 @@
+'''
+Android accelerometer
+---------------------
+'''
+
+from plyer.facades import Accelerometer
+from jnius import PythonJavaClass, java_method, autoclass, cast
+from plyer.platforms.android import activity
+
+Context = autoclass('android.content.Context')
+Sensor = autoclass('android.hardware.Sensor')
+SensorManager = autoclass('android.hardware.SensorManager')
+
+
+class AccelerometerSensorListener(PythonJavaClass):
+ __javainterfaces__ = ['android/hardware/SensorEventListener']
+
+ def __init__(self):
+ super().__init__()
+ self.SensorManager = cast(
+ 'android.hardware.SensorManager',
+ activity.getSystemService(Context.SENSOR_SERVICE)
+ )
+ self.sensor = self.SensorManager.getDefaultSensor(
+ Sensor.TYPE_ACCELEROMETER
+ )
+
+ self.values = [None, None, None]
+
+ def enable(self):
+ self.SensorManager.registerListener(
+ self, self.sensor,
+ SensorManager.SENSOR_DELAY_NORMAL
+ )
+
+ def disable(self):
+ self.SensorManager.unregisterListener(self, self.sensor)
+
+ @java_method('(Landroid/hardware/SensorEvent;)V')
+ def onSensorChanged(self, event):
+ self.values = event.values[:3]
+
+ @java_method('(Landroid/hardware/Sensor;I)V')
+ def onAccuracyChanged(self, sensor, accuracy):
+ # Maybe, do something in future?
+ pass
+
+
+class AndroidAccelerometer(Accelerometer):
+ def __init__(self):
+ super().__init__()
+ self.bState = False
+
+ def _enable(self):
+ if (not self.bState):
+ self.listener = AccelerometerSensorListener()
+ self.listener.enable()
+ self.bState = True
+
+ def _disable(self):
+ if (self.bState):
+ self.bState = False
+ self.listener.disable()
+ del self.listener
+
+ def _get_acceleration(self):
+ if (self.bState):
+ return tuple(self.listener.values)
+ else:
+ return (None, None, None)
+
+ def __del__(self):
+ if(self.bState):
+ self._disable()
+ super().__del__()
+
+
+def instance():
+ return AndroidAccelerometer()
diff --git a/sbapp/plyer/platforms/android/audio.py b/sbapp/plyer/platforms/android/audio.py
new file mode 100644
index 00000000..9f000ff4
--- /dev/null
+++ b/sbapp/plyer/platforms/android/audio.py
@@ -0,0 +1,58 @@
+from jnius import autoclass
+
+from plyer.facades.audio import Audio
+
+# Recorder Classes
+MediaRecorder = autoclass('android.media.MediaRecorder')
+AudioSource = autoclass('android.media.MediaRecorder$AudioSource')
+OutputFormat = autoclass('android.media.MediaRecorder$OutputFormat')
+AudioEncoder = autoclass('android.media.MediaRecorder$AudioEncoder')
+
+# Player Classes
+MediaPlayer = autoclass('android.media.MediaPlayer')
+
+
+class AndroidAudio(Audio):
+ '''Audio for android.
+
+ For recording audio we use MediaRecorder Android class.
+ For playing audio we use MediaPlayer Android class.
+ '''
+
+ def __init__(self, file_path=None):
+ default_path = '/sdcard/testrecorder.3gp'
+ super().__init__(file_path or default_path)
+
+ self._recorder = None
+ self._player = None
+
+ def _start(self):
+ self._recorder = MediaRecorder()
+ self._recorder.setAudioSource(AudioSource.DEFAULT)
+ self._recorder.setOutputFormat(OutputFormat.DEFAULT)
+ self._recorder.setAudioEncoder(AudioEncoder.DEFAULT)
+ self._recorder.setOutputFile(self.file_path)
+
+ self._recorder.prepare()
+ self._recorder.start()
+
+ def _stop(self):
+ if self._recorder:
+ self._recorder.stop()
+ self._recorder.release()
+ self._recorder = None
+
+ if self._player:
+ self._player.stop()
+ self._player.release()
+ self._player = None
+
+ def _play(self):
+ self._player = MediaPlayer()
+ self._player.setDataSource(self.file_path)
+ self._player.prepare()
+ self._player.start()
+
+
+def instance():
+ return AndroidAudio()
diff --git a/sbapp/plyer/platforms/android/barometer.py b/sbapp/plyer/platforms/android/barometer.py
new file mode 100644
index 00000000..5b9ed486
--- /dev/null
+++ b/sbapp/plyer/platforms/android/barometer.py
@@ -0,0 +1,65 @@
+from jnius import autoclass
+from jnius import cast
+from jnius import java_method
+from jnius import PythonJavaClass
+
+from plyer.facades import Barometer
+from plyer.platforms.android import activity
+
+ActivityInfo = autoclass('android.content.pm.ActivityInfo')
+Context = autoclass('android.content.Context')
+Sensor = autoclass('android.hardware.Sensor')
+SensorManager = autoclass('android.hardware.SensorManager')
+
+
+class BarometerSensorListener(PythonJavaClass):
+ __javainterfaces__ = ['android/hardware/SensorEventListener']
+
+ def __init__(self):
+ super().__init__()
+ service = activity.getSystemService(Context.SENSOR_SERVICE)
+ self.SensorManager = cast('android.hardware.SensorManager', service)
+
+ self.sensor = self.SensorManager.getDefaultSensor(Sensor.TYPE_PRESSURE)
+ self.value = None
+
+ def enable(self):
+ self.SensorManager.registerListener(
+ self, self.sensor,
+ SensorManager.SENSOR_DELAY_NORMAL
+ )
+
+ def disable(self):
+ self.SensorManager.unregisterListener(self, self.sensor)
+
+ @java_method('(Landroid/hardware/SensorEvent;)V')
+ def onSensorChanged(self, event):
+ self.value = event.values[0]
+
+ @java_method('(Landroid/hardware/Sensor;I)V')
+ def onAccuracyChanged(self, sensor, accuracy):
+ pass
+
+
+class AndroidBarometer(Barometer):
+
+ listener = None
+
+ def _get_pressure(self):
+ if self.listener and self.listener.value:
+ pressure = self.listener.value
+ return pressure
+
+ def _enable(self):
+ if not self.listener:
+ self.listener = BarometerSensorListener()
+ self.listener.enable()
+
+ def _disable(self):
+ if self.listener:
+ self.listener.disable()
+ delattr(self, 'listener')
+
+
+def instance():
+ return AndroidBarometer()
diff --git a/sbapp/plyer/platforms/android/battery.py b/sbapp/plyer/platforms/android/battery.py
new file mode 100644
index 00000000..4a58f1f0
--- /dev/null
+++ b/sbapp/plyer/platforms/android/battery.py
@@ -0,0 +1,47 @@
+'''
+Module of Android API for plyer.battery.
+'''
+
+from jnius import autoclass, cast
+from plyer.platforms.android import activity
+from plyer.facades import Battery
+
+Intent = autoclass('android.content.Intent')
+BatteryManager = autoclass('android.os.BatteryManager')
+IntentFilter = autoclass('android.content.IntentFilter')
+
+
+class AndroidBattery(Battery):
+ '''
+ Implementation of Android battery API.
+ '''
+
+ def _get_state(self):
+ status = {"isCharging": None, "percentage": None}
+
+ ifilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
+
+ battery_status = cast(
+ 'android.content.Intent',
+ activity.registerReceiver(None, ifilter)
+ )
+
+ query = battery_status.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
+ is_charging = query == BatteryManager.BATTERY_STATUS_CHARGING
+ is_full = query == BatteryManager.BATTERY_STATUS_FULL
+
+ level = battery_status.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
+ scale = battery_status.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
+ percentage = (level / float(scale)) * 100
+
+ status['isCharging'] = is_charging or is_full
+ status['percentage'] = percentage
+
+ return status
+
+
+def instance():
+ '''
+ Instance for facade proxy.
+ '''
+ return AndroidBattery()
diff --git a/sbapp/plyer/platforms/android/bluetooth.py b/sbapp/plyer/platforms/android/bluetooth.py
new file mode 100644
index 00000000..de72393b
--- /dev/null
+++ b/sbapp/plyer/platforms/android/bluetooth.py
@@ -0,0 +1,32 @@
+'''
+Module of Android API for plyer.bluetooth.
+'''
+
+from jnius import autoclass
+from plyer.platforms.android import activity
+from plyer.facades import Bluetooth
+
+Global = autoclass('android.provider.Settings$Global')
+
+
+class AndroidBluetooth(Bluetooth):
+ '''
+ Implementation of Android Bluetooth API.
+ '''
+
+ def _get_info(self):
+ bluetooth_enabled = Global.getString(
+ activity.getContentResolver(),
+ Global.BLUETOOTH_ON
+ )
+ status = 'off'
+ if bluetooth_enabled:
+ status = 'on'
+ return status
+
+
+def instance():
+ '''
+ Instance for facade proxy.
+ '''
+ return AndroidBluetooth()
diff --git a/sbapp/plyer/platforms/android/brightness.py b/sbapp/plyer/platforms/android/brightness.py
new file mode 100755
index 00000000..d0a1157f
--- /dev/null
+++ b/sbapp/plyer/platforms/android/brightness.py
@@ -0,0 +1,34 @@
+'''
+Android Brightness
+------------------
+'''
+
+from jnius import autoclass
+from plyer.facades import Brightness
+from android import mActivity
+
+System = autoclass('android.provider.Settings$System')
+
+
+class AndroidBrightness(Brightness):
+
+ def _current_level(self):
+
+ System.putInt(
+ mActivity.getContentResolver(),
+ System.SCREEN_BRIGHTNESS_MODE,
+ System.SCREEN_BRIGHTNESS_MODE_MANUAL)
+ cr_level = System.getInt(
+ mActivity.getContentResolver(),
+ System.SCREEN_BRIGHTNESS)
+ return (cr_level / 255.) * 100
+
+ def _set_level(self, level):
+ System.putInt(
+ mActivity.getContentResolver(),
+ System.SCREEN_BRIGHTNESS,
+ (level / 100.) * 255)
+
+
+def instance():
+ return AndroidBrightness()
diff --git a/sbapp/plyer/platforms/android/call.py b/sbapp/plyer/platforms/android/call.py
new file mode 100644
index 00000000..2a1388c7
--- /dev/null
+++ b/sbapp/plyer/platforms/android/call.py
@@ -0,0 +1,29 @@
+'''
+Android Call
+-----------
+'''
+
+from jnius import autoclass
+from plyer.facades import Call
+from plyer.platforms.android import activity
+
+Intent = autoclass('android.content.Intent')
+uri = autoclass('android.net.Uri')
+
+
+class AndroidCall(Call):
+
+ def _makecall(self, **kwargs):
+
+ intent = Intent(Intent.ACTION_CALL)
+ tel = kwargs.get('tel')
+ intent.setData(uri.parse("tel:{}".format(tel)))
+ activity.startActivity(intent)
+
+ def _dialcall(self, **kwargs):
+ intent_ = Intent(Intent.ACTION_DIAL)
+ activity.startActivity(intent_)
+
+
+def instance():
+ return AndroidCall()
diff --git a/sbapp/plyer/platforms/android/camera.py b/sbapp/plyer/platforms/android/camera.py
new file mode 100644
index 00000000..19707bdc
--- /dev/null
+++ b/sbapp/plyer/platforms/android/camera.py
@@ -0,0 +1,59 @@
+import android
+import android.activity
+from os import remove
+from jnius import autoclass, cast
+from plyer.facades import Camera
+from plyer.platforms.android import activity
+
+Intent = autoclass('android.content.Intent')
+PythonActivity = autoclass('org.kivy.android.PythonActivity')
+MediaStore = autoclass('android.provider.MediaStore')
+Uri = autoclass('android.net.Uri')
+
+
+class AndroidCamera(Camera):
+
+ def _take_picture(self, on_complete, filename=None):
+ assert(on_complete is not None)
+ self.on_complete = on_complete
+ self.filename = filename
+ android.activity.unbind(on_activity_result=self._on_activity_result)
+ android.activity.bind(on_activity_result=self._on_activity_result)
+ intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
+ uri = Uri.parse('file://' + filename)
+ parcelable = cast('android.os.Parcelable', uri)
+ intent.putExtra(MediaStore.EXTRA_OUTPUT, parcelable)
+ activity.startActivityForResult(intent, 0x123)
+
+ def _take_video(self, on_complete, filename=None):
+ assert(on_complete is not None)
+ self.on_complete = on_complete
+ self.filename = filename
+ android.activity.unbind(on_activity_result=self._on_activity_result)
+ android.activity.bind(on_activity_result=self._on_activity_result)
+ intent = Intent(MediaStore.ACTION_VIDEO_CAPTURE)
+ uri = Uri.parse('file://' + filename)
+ parcelable = cast('android.os.Parcelable', uri)
+ intent.putExtra(MediaStore.EXTRA_OUTPUT, parcelable)
+
+ # 0 = low quality, suitable for MMS messages,
+ # 1 = high quality
+ intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1)
+ activity.startActivityForResult(intent, 0x123)
+
+ def _on_activity_result(self, requestCode, resultCode, intent):
+ if requestCode != 0x123:
+ return
+ android.activity.unbind(on_activity_result=self._on_activity_result)
+ if self.on_complete(self.filename):
+ self._remove(self.filename)
+
+ def _remove(self, fn):
+ try:
+ remove(fn)
+ except OSError:
+ pass
+
+
+def instance():
+ return AndroidCamera()
diff --git a/sbapp/plyer/platforms/android/compass.py b/sbapp/plyer/platforms/android/compass.py
new file mode 100644
index 00000000..fcf0483c
--- /dev/null
+++ b/sbapp/plyer/platforms/android/compass.py
@@ -0,0 +1,119 @@
+'''
+Android Compass
+---------------------
+'''
+
+from plyer.facades import Compass
+from jnius import PythonJavaClass, java_method, autoclass, cast
+from plyer.platforms.android import activity
+
+Context = autoclass('android.content.Context')
+Sensor = autoclass('android.hardware.Sensor')
+SensorManager = autoclass('android.hardware.SensorManager')
+
+
+class MFUSensorListener(PythonJavaClass):
+ __javainterfaces__ = ['android/hardware/SensorEventListener']
+
+ def __init__(self):
+ super().__init__()
+ service = activity.getSystemService(Context.SENSOR_SERVICE)
+ self.SensorManager = cast('android.hardware.SensorManager', service)
+
+ self.sensor = self.SensorManager.getDefaultSensor(
+ Sensor.TYPE_MAGNETIC_FIELD_UNCALIBRATED)
+ self.values = [None, None, None, None, None, None]
+
+ def enable(self):
+ self.SensorManager.registerListener(
+ self, self.sensor,
+ SensorManager.SENSOR_DELAY_NORMAL
+ )
+
+ def disable(self):
+ self.SensorManager.unregisterListener(self, self.sensor)
+
+ @java_method('(Landroid/hardware/SensorEvent;)V')
+ def onSensorChanged(self, event):
+ self.values = event.values[:6]
+
+ @java_method('(Landroid/hardware/Sensor;I)V')
+ def onAccuracyChanged(self, sensor, accuracy):
+ pass
+
+
+class MagneticFieldSensorListener(PythonJavaClass):
+ __javainterfaces__ = ['android/hardware/SensorEventListener']
+
+ def __init__(self):
+ super().__init__()
+ self.SensorManager = cast(
+ 'android.hardware.SensorManager',
+ activity.getSystemService(Context.SENSOR_SERVICE)
+ )
+ self.sensor = self.SensorManager.getDefaultSensor(
+ Sensor.TYPE_MAGNETIC_FIELD
+ )
+
+ self.values = [None, None, None]
+
+ def enable(self):
+ self.SensorManager.registerListener(
+ self, self.sensor,
+ SensorManager.SENSOR_DELAY_NORMAL
+ )
+
+ def disable(self):
+ self.SensorManager.unregisterListener(self, self.sensor)
+
+ @java_method('(Landroid/hardware/SensorEvent;)V')
+ def onSensorChanged(self, event):
+ self.values = event.values[:3]
+
+ @java_method('(Landroid/hardware/Sensor;I)V')
+ def onAccuracyChanged(self, sensor, accuracy):
+ # Maybe, do something in future?
+ pass
+
+
+class AndroidCompass(Compass):
+ def __init__(self):
+ super().__init__()
+ self.bState = False
+
+ def _enable(self):
+ if (not self.bState):
+ self.listenerm = MagneticFieldSensorListener()
+ self.listenermu = MFUSensorListener()
+ self.listenerm.enable()
+ self.listenermu.enable()
+ self.bState = True
+
+ def _disable(self):
+ if (self.bState):
+ self.bState = False
+ self.listenerm.disable()
+ self.listenermu.disable()
+ del self.listenerm
+ del self.listenermu
+
+ def _get_orientation(self):
+ if (self.bState):
+ return tuple(self.listenerm.values)
+ else:
+ return (None, None, None)
+
+ def _get_field_uncalib(self):
+ if (self.bState):
+ return tuple(self.listenermu.values)
+ else:
+ return (None, None, None, None, None, None)
+
+ def __del__(self):
+ if(self.bState):
+ self._disable()
+ super().__del__()
+
+
+def instance():
+ return AndroidCompass()
diff --git a/sbapp/plyer/platforms/android/devicename.py b/sbapp/plyer/platforms/android/devicename.py
new file mode 100644
index 00000000..75855cc7
--- /dev/null
+++ b/sbapp/plyer/platforms/android/devicename.py
@@ -0,0 +1,33 @@
+'''
+Module of Android API for plyer.devicename.
+'''
+
+from jnius import autoclass
+from plyer.facades import DeviceName
+
+Build = autoclass('android.os.Build')
+
+
+class AndroidDeviceName(DeviceName):
+ '''
+ Implementation of Android devicename API.
+ '''
+
+ def _get_device_name(self):
+ """
+ Method to get the device name aka model in an android environment.
+
+ Changed the implementation from 'android.provider.Settings.Global' to
+ 'android.os.Build' because 'android.provider.Settings.Global' was
+ introduced in API 17 whereas 'android.os.Build' is present since API 1
+
+ Thereby making this method more backward compatible.
+ """
+ return Build.MODEL
+
+
+def instance():
+ '''
+ Instance for facade proxy.
+ '''
+ return AndroidDeviceName()
diff --git a/sbapp/plyer/platforms/android/email.py b/sbapp/plyer/platforms/android/email.py
new file mode 100644
index 00000000..95dfa5ee
--- /dev/null
+++ b/sbapp/plyer/platforms/android/email.py
@@ -0,0 +1,58 @@
+'''
+Module of Android API for plyer.email.
+'''
+
+from jnius import autoclass, cast
+from plyer.facades import Email
+from plyer.platforms.android import activity
+
+Intent = autoclass('android.content.Intent')
+AndroidString = autoclass('java.lang.String')
+
+
+class AndroidEmail(Email):
+ '''
+ Implementation of Android email API.
+ '''
+
+ def _send(self, **kwargs):
+ intent = Intent(Intent.ACTION_SEND)
+ intent.setType('text/plain')
+
+ recipient = kwargs.get('recipient')
+ subject = kwargs.get('subject')
+ text = kwargs.get('text')
+ create_chooser = kwargs.get('create_chooser')
+
+ if recipient:
+ intent.putExtra(Intent.EXTRA_EMAIL, [recipient])
+ if subject:
+ android_subject = cast(
+ 'java.lang.CharSequence',
+ AndroidString(subject)
+ )
+ intent.putExtra(Intent.EXTRA_SUBJECT, android_subject)
+ if text:
+ android_text = cast(
+ 'java.lang.CharSequence',
+ AndroidString(text)
+ )
+ intent.putExtra(Intent.EXTRA_TEXT, android_text)
+
+ if create_chooser:
+ chooser_title = cast(
+ 'java.lang.CharSequence',
+ AndroidString('Send message with:')
+ )
+ activity.startActivity(
+ Intent.createChooser(intent, chooser_title)
+ )
+ else:
+ activity.startActivity(intent)
+
+
+def instance():
+ '''
+ Instance for facade proxy.
+ '''
+ return AndroidEmail()
diff --git a/sbapp/plyer/platforms/android/filechooser.py b/sbapp/plyer/platforms/android/filechooser.py
new file mode 100644
index 00000000..b809ccbb
--- /dev/null
+++ b/sbapp/plyer/platforms/android/filechooser.py
@@ -0,0 +1,447 @@
+'''
+Android file chooser
+--------------------
+
+Android runs ``Activity`` asynchronously via pausing our ``PythonActivity``
+and starting a new one in the foreground. This means
+``AndroidFileChooser._open_file()`` will always return the default value of
+``AndroidFileChooser.selection`` i.e. ``None``.
+
+After the ``Activity`` (for us it's the file chooser ``Intent``) is completed,
+Android moves it to the background (or destroys or whatever is implemented)
+and pushes ``PythonActivity`` to the foreground.
+
+We have a custom listener for ``android.app.Activity.onActivityResult()``
+via `android` package from `python-for-android` recipe,
+``AndroidFileChooser._on_activity_result()`` which is called independently of
+any our action (we may call anything from our application in Python and this
+handler will be called nevertheless on each ``android.app.Activity`` result
+in the system).
+
+In the handler we check if the ``request_code`` matches the code passed to the
+``Context.startActivityForResult()`` i.e. if the result from
+``android.app.Activity`` is indeed meant for our ``PythonActivity`` and then we
+proceed.
+
+Since the ``android.app.Activity.onActivityResult()`` is the only way for us
+to intercept the result and we have a handler bound via ``android`` package,
+we need to get the path/file/... selection to the user the same way.
+
+Threading + ``Thread.join()`` or ``time.sleep()`` or any other kind of waiting
+for the result is not an option because:
+
+1) ``android.app.Activity.onActivityResult()`` might remain unexecuted if
+the launched file chooser activity does not return the result (``Activity``
+dies/freezes/etc).
+
+2) Thread will be still waiting for the result e.g. an update of a value or
+to actually finish, however the result from the call of
+``AndroidFileChooser._open_file()`` will be returned nevertheless and anything
+using that result will use an incorrect one i.e. the default value of
+``AndroidFilechooser.selection`` (``None``).
+
+.. versionadded:: 1.4.0
+'''
+
+from os.path import join, basename
+from random import randint
+
+from android import activity, mActivity
+from jnius import autoclass, cast, JavaException
+from plyer.facades import FileChooser
+from plyer import storagepath
+
+Environment = autoclass("android.os.Environment")
+String = autoclass('java.lang.String')
+Intent = autoclass('android.content.Intent')
+Activity = autoclass('android.app.Activity')
+DocumentsContract = autoclass('android.provider.DocumentsContract')
+ContentUris = autoclass('android.content.ContentUris')
+Uri = autoclass('android.net.Uri')
+Long = autoclass('java.lang.Long')
+IMedia = autoclass('android.provider.MediaStore$Images$Media')
+VMedia = autoclass('android.provider.MediaStore$Video$Media')
+AMedia = autoclass('android.provider.MediaStore$Audio$Media')
+
+
+class AndroidFileChooser(FileChooser):
+ '''
+ FileChooser implementation for Android using
+ the built-in file browser via Intent.
+
+ .. versionadded:: 1.4.0
+ '''
+
+ # filechooser activity <-> result pair identification
+ select_code = None
+
+ # default selection value
+ selection = None
+
+ # select multiple files
+ multiple = False
+
+ # mime types
+ mime_type = {
+ "doc": "application/msword",
+ "docx": "application/vnd.openxmlformats-officedocument." +
+ "wordprocessingml.document",
+ "ppt": "application/vnd.ms-powerpoint",
+ "pptx": "application/vnd.openxmlformats-officedocument." +
+ "presentationml.presentation",
+ "xls": "application/vnd.ms-excel",
+ "xlsx": "application/vnd.openxmlformats-officedocument." +
+ "spreadsheetml.sheet",
+ "text": "text/*",
+ "pdf": "application/pdf",
+ "zip": "application/zip",
+ "image": "image/*",
+ "video": "video/*",
+ "audio": "audio/*",
+ "application": "application/*"}
+
+ selected_mime_type = None
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.select_code = randint(123456, 654321)
+ self.selection = None
+
+ # bind a function for a response from filechooser activity
+ activity.bind(on_activity_result=self._on_activity_result)
+
+ @staticmethod
+ def _handle_selection(selection):
+ '''
+ Dummy placeholder for returning selection from
+ ``android.app.Activity.onActivityResult()``.
+
+ .. versionadded:: 1.4.0
+ '''
+ return selection
+
+ def _open_file(self, **kwargs):
+ '''
+ Running Android Activity is non-blocking and the only call
+ that blocks is onActivityResult running in GUI thread
+
+ .. versionadded:: 1.4.0
+ '''
+
+ # set up selection handler
+ # startActivityForResult is async
+ # onActivityResult is sync
+ self._handle_selection = kwargs.pop(
+ 'on_selection', self._handle_selection
+ )
+ self.selected_mime_type = \
+ kwargs.pop("filters")[0] if "filters" in kwargs else ""
+
+ # create Intent for opening
+ file_intent = Intent(Intent.ACTION_GET_CONTENT)
+ if not self.selected_mime_type or \
+ type(self.selected_mime_type) != str or \
+ self.selected_mime_type not in self.mime_type:
+ file_intent.setType("*/*")
+ else:
+ file_intent.setType(self.mime_type[self.selected_mime_type])
+ file_intent.addCategory(
+ Intent.CATEGORY_OPENABLE
+ )
+
+ # use putExtra to allow multiple file selection
+ if kwargs.get('multiple', self.multiple):
+ file_intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, True)
+
+ # start a new activity from PythonActivity
+ # which creates a filechooser via intent
+ mActivity.startActivityForResult(
+ Intent.createChooser(file_intent, cast(
+ 'java.lang.CharSequence',
+ String("FileChooser")
+ )),
+ self.select_code
+ )
+
+ def _on_activity_result(self, request_code, result_code, data):
+ '''
+ Listener for ``android.app.Activity.onActivityResult()`` assigned
+ via ``android.activity.bind()``.
+
+ .. versionadded:: 1.4.0
+ '''
+
+ # not our response
+ if request_code != self.select_code:
+ return
+
+ if result_code != Activity.RESULT_OK:
+ # The action had been cancelled.
+ return
+
+ selection = []
+ # Process multiple URI if multiple files selected
+ try:
+ for count in range(data.getClipData().getItemCount()):
+ ele = self._resolve_uri(
+ data.getClipData().getItemAt(count).getUri()) or []
+ selection.append(ele)
+ except Exception:
+ selection = [self._resolve_uri(data.getData()), ]
+
+ # return value to object
+ self.selection = selection
+ # return value via callback
+ self._handle_selection(selection)
+
+ @staticmethod
+ def _handle_external_documents(uri):
+ '''
+ Selection from the system filechooser when using ``Phone``
+ or ``Internal storage`` or ``SD card`` option from menu.
+
+ .. versionadded:: 1.4.0
+ '''
+
+ file_id = DocumentsContract.getDocumentId(uri)
+ file_type, file_name = file_id.split(':')
+
+ # internal SD card mostly mounted as a files storage in phone
+ internal = storagepath.get_external_storage_dir()
+
+ # external (removable) SD card i.e. microSD
+ external = storagepath.get_sdcard_dir()
+ try:
+ external_base = basename(external)
+ except TypeError:
+ external_base = basename(internal)
+
+ # resolve sdcard path
+ sd_card = internal
+
+ # because external might have /storage/.../1 or other suffix
+ # and file_type might be only a part of the real folder in /storage
+ if file_type in external_base or external_base in file_type:
+ sd_card = external
+ elif file_type == "home":
+ sd_card = join(Environment.getExternalStorageDirectory(
+ ).getAbsolutePath(), Environment.DIRECTORY_DOCUMENTS)
+
+ return join(sd_card, file_name)
+
+ @staticmethod
+ def _handle_media_documents(uri):
+ '''
+ Selection from the system filechooser when using ``Images``
+ or ``Videos`` or ``Audio`` option from menu.
+
+ .. versionadded:: 1.4.0
+ '''
+
+ file_id = DocumentsContract.getDocumentId(uri)
+ file_type, file_name = file_id.split(':')
+ selection = '_id=?'
+
+ if file_type == 'image':
+ uri = IMedia.EXTERNAL_CONTENT_URI
+ elif file_type == 'video':
+ uri = VMedia.EXTERNAL_CONTENT_URI
+ elif file_type == 'audio':
+ uri = AMedia.EXTERNAL_CONTENT_URI
+ return file_name, selection, uri
+
+ @staticmethod
+ def _handle_downloads_documents(uri):
+ '''
+ Selection from the system filechooser when using ``Downloads``
+ option from menu. Might not work all the time due to:
+
+ 1) invalid URI:
+
+ jnius.jnius.JavaException:
+ JVM exception occurred: Unknown URI:
+ content://downloads/public_downloads/1034
+
+ 2) missing URI / android permissions
+
+ jnius.jnius.JavaException:
+ JVM exception occurred:
+ Permission Denial: reading
+ com.android.providers.downloads.DownloadProvider uri
+ content://downloads/all_downloads/1034 from pid=2532, uid=10455
+ requires android.permission.ACCESS_ALL_DOWNLOADS,
+ or grantUriPermission()
+
+ Workaround:
+ Selecting path from ``Phone`` -> ``Download`` -> ``<file>``
+ (or ``Internal storage``) manually.
+
+ .. versionadded:: 1.4.0
+ '''
+
+ # known locations, differ between machines
+ downloads = [
+ 'content://downloads/public_downloads',
+ 'content://downloads/my_downloads',
+
+ # all_downloads requires separate permission
+ # android.permission.ACCESS_ALL_DOWNLOADS
+ 'content://downloads/all_downloads'
+ ]
+
+ file_id = DocumentsContract.getDocumentId(uri)
+ try_uris = [
+ ContentUris.withAppendedId(
+ Uri.parse(down), Long.valueOf(file_id)
+ )
+ for down in downloads
+ ]
+
+ # try all known Download folder uris
+ # and handle JavaExceptions due to different locations
+ # for content:// downloads or missing permission
+ path = None
+ for down in try_uris:
+ try:
+ path = AndroidFileChooser._parse_content(
+ uri=down, projection=['_data'],
+ selection=None,
+ selection_args=None,
+ sort_order=None
+ )
+
+ except JavaException:
+ import traceback
+ traceback.print_exc()
+
+ # we got a path, ignore the rest
+ if path:
+ break
+
+ # alternative approach to Downloads by joining
+ # all data items from Activity result
+ if not path:
+ for down in try_uris:
+ try:
+ path = AndroidFileChooser._parse_content(
+ uri=down, projection=None,
+ selection=None,
+ selection_args=None,
+ sort_order=None,
+ index_all=True
+ )
+
+ except JavaException:
+ import traceback
+ traceback.print_exc()
+
+ # we got a path, ignore the rest
+ if path:
+ break
+ return path
+
+ def _resolve_uri(self, uri):
+ '''
+ Resolve URI input from ``android.app.Activity.onActivityResult()``.
+
+ .. versionadded:: 1.4.0
+ '''
+
+ uri_authority = uri.getAuthority()
+ uri_scheme = uri.getScheme().lower()
+
+ path = None
+ file_name = None
+ selection = None
+ downloads = None
+
+ # This does not allow file selected from google photos or gallery
+ # or even any other file explorer to work
+ # not a document URI, nothing to convert from
+ # if not DocumentsContract.isDocumentUri(mActivity, uri):
+ # return path
+
+ if uri_authority == 'com.android.externalstorage.documents':
+ return self._handle_external_documents(uri)
+
+ # in case a user selects a file from 'Downloads' section
+ # note: this won't be triggered if a user selects a path directly
+ # e.g.: Phone -> Download -> <some file>
+ elif uri_authority == 'com.android.providers.downloads.documents':
+ path = downloads = self._handle_downloads_documents(uri)
+
+ elif uri_authority == 'com.android.providers.media.documents':
+ file_name, selection, uri = self._handle_media_documents(uri)
+
+ # parse content:// scheme to path
+ if uri_scheme == 'content' and not downloads:
+ try:
+ path = self._parse_content(
+ uri=uri, projection=['_data'], selection=selection,
+ selection_args=file_name, sort_order=None
+ )
+ except JavaException: # handles array error for selection_args
+ path = self._parse_content(
+ uri=uri, projection=['_data'], selection=selection,
+ selection_args=[file_name], sort_order=None
+ )
+
+ # nothing to parse, file:// will return a proper path
+ elif uri_scheme == 'file':
+ path = uri.getPath()
+
+ return path
+
+ @staticmethod
+ def _parse_content(
+ uri, projection, selection, selection_args, sort_order,
+ index_all=False
+ ):
+ '''
+ Parser for ``content://`` URI returned by some Android resources.
+
+ .. versionadded:: 1.4.0
+ '''
+
+ result = None
+ resolver = mActivity.getContentResolver()
+ read = Intent.FLAG_GRANT_READ_URI_PERMISSION
+ write = Intent.FLAG_GRANT_READ_URI_PERMISSION
+ persist = Intent.FLAG_GRANT_READ_URI_PERMISSION
+
+ # grant permission for our activity
+ mActivity.grantUriPermission(
+ mActivity.getPackageName(),
+ uri,
+ read | write | persist
+ )
+
+ if not index_all:
+ cursor = resolver.query(
+ uri, projection, selection,
+ selection_args, sort_order
+ )
+
+ idx = cursor.getColumnIndex(projection[0])
+ if idx != -1 and cursor.moveToFirst():
+ result = cursor.getString(idx)
+ else:
+ result = []
+ cursor = resolver.query(
+ uri, projection, selection,
+ selection_args, sort_order
+ )
+ while cursor.moveToNext():
+ for idx in range(cursor.getColumnCount()):
+ result.append(cursor.getString(idx))
+ result = '/'.join(result)
+ return result
+
+ def _file_selection_dialog(self, **kwargs):
+ mode = kwargs.pop('mode', None)
+ if mode == 'open':
+ self._open_file(**kwargs)
+
+
+def instance():
+ return AndroidFileChooser()
diff --git a/sbapp/plyer/platforms/android/flash.py b/sbapp/plyer/platforms/android/flash.py
new file mode 100644
index 00000000..eec1ae38
--- /dev/null
+++ b/sbapp/plyer/platforms/android/flash.py
@@ -0,0 +1,55 @@
+# coding=utf-8
+"""
+Flash
+-----
+"""
+
+from plyer.facades import Flash
+from jnius import autoclass
+from plyer.platforms.android import activity
+
+Camera = autoclass("android.hardware.Camera")
+CameraParameters = autoclass("android.hardware.Camera$Parameters")
+SurfaceTexture = autoclass("android.graphics.SurfaceTexture")
+PackageManager = autoclass('android.content.pm.PackageManager')
+pm = activity.getPackageManager()
+flash_available = pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH)
+
+
+class AndroidFlash(Flash):
+ _camera = None
+
+ def _on(self):
+ if self._camera is None:
+ self._camera_open()
+ if not self._camera:
+ return
+ self._camera.setParameters(self._f_on)
+
+ def _off(self):
+ if not self._camera:
+ return
+ self._camera.setParameters(self._f_off)
+
+ def _release(self):
+ if not self._camera:
+ return
+ self._camera.stopPreview()
+ self._camera.release()
+ self._camera = None
+
+ def _camera_open(self):
+ if not flash_available:
+ return
+ self._camera = Camera.open()
+ self._f_on = Camera.getParameters()
+ self._f_off = Camera.getParameters()
+ self._f_on.setFlashMode(CameraParameters.FLASH_MODE_TORCH)
+ self._f_off.setFlashMode(CameraParameters.FLASH_MODE_OFF)
+ self._camera.startPreview()
+ # Need this for Nexus 5
+ self._camera.setPreviewTexture(SurfaceTexture(0))
+
+
+def instance():
+ return AndroidFlash()
diff --git a/sbapp/plyer/platforms/android/gps.py b/sbapp/plyer/platforms/android/gps.py
new file mode 100644
index 00000000..17fd86e9
--- /dev/null
+++ b/sbapp/plyer/platforms/android/gps.py
@@ -0,0 +1,82 @@
+'''
+Android GPS
+-----------
+'''
+
+from plyer.facades import GPS
+from plyer.platforms.android import activity
+from jnius import autoclass, java_method, PythonJavaClass
+
+Looper = autoclass('android.os.Looper')
+LocationManager = autoclass('android.location.LocationManager')
+Context = autoclass('android.content.Context')
+
+
+class _LocationListener(PythonJavaClass):
+ __javainterfaces__ = ['android/location/LocationListener']
+
+ def __init__(self, root):
+ self.root = root
+ super().__init__()
+
+ @java_method('(Landroid/location/Location;)V')
+ def onLocationChanged(self, location):
+ self.root.on_location(
+ lat=location.getLatitude(),
+ lon=location.getLongitude(),
+ speed=location.getSpeed(),
+ bearing=location.getBearing(),
+ altitude=location.getAltitude(),
+ accuracy=location.getAccuracy())
+
+ @java_method('(Ljava/lang/String;)V')
+ def onProviderEnabled(self, status):
+ if self.root.on_status:
+ self.root.on_status('provider-enabled', status)
+
+ @java_method('(Ljava/lang/String;)V')
+ def onProviderDisabled(self, status):
+ if self.root.on_status:
+ self.root.on_status('provider-disabled', status)
+
+ @java_method('(Ljava/lang/String;ILandroid/os/Bundle;)V')
+ def onStatusChanged(self, provider, status, extras):
+ if self.root.on_status:
+ s_status = 'unknown'
+ if status == 0x00:
+ s_status = 'out-of-service'
+ elif status == 0x01:
+ s_status = 'temporarily-unavailable'
+ elif status == 0x02:
+ s_status = 'available'
+ self.root.on_status('provider-status', '{}: {}'.format(
+ provider, s_status))
+
+
+class AndroidGPS(GPS):
+
+ def _configure(self):
+ if not hasattr(self, '_location_manager'):
+ self._location_manager = activity.getSystemService(
+ Context.LOCATION_SERVICE
+ )
+ self._location_listener = _LocationListener(self)
+
+ def _start(self, **kwargs):
+ min_time = kwargs.get('minTime')
+ min_distance = kwargs.get('minDistance')
+ providers = self._location_manager.getProviders(False).toArray()
+ for provider in providers:
+ self._location_manager.requestLocationUpdates(
+ provider,
+ min_time, # minTime, in milliseconds
+ min_distance, # minDistance, in meters
+ self._location_listener,
+ Looper.getMainLooper())
+
+ def _stop(self):
+ self._location_manager.removeUpdates(self._location_listener)
+
+
+def instance():
+ return AndroidGPS()
diff --git a/sbapp/plyer/platforms/android/gravity.py b/sbapp/plyer/platforms/android/gravity.py
new file mode 100644
index 00000000..9cb4100b
--- /dev/null
+++ b/sbapp/plyer/platforms/android/gravity.py
@@ -0,0 +1,84 @@
+'''
+Android gravity
+---------------------
+'''
+
+from jnius import autoclass
+from jnius import cast
+from jnius import java_method
+from jnius import PythonJavaClass
+
+from plyer.facades import Gravity
+from plyer.platforms.android import activity
+
+Context = autoclass('android.content.Context')
+Sensor = autoclass('android.hardware.Sensor')
+SensorManager = autoclass('android.hardware.SensorManager')
+
+
+class GravitySensorListener(PythonJavaClass):
+ __javainterfaces__ = ['android/hardware/SensorEventListener']
+
+ def __init__(self):
+ super().__init__()
+
+ service = activity.getSystemService(Context.SENSOR_SERVICE)
+ self.SensorManager = cast('android.hardware.SensorManager', service)
+
+ self.sensor = self.SensorManager.getDefaultSensor(
+ Sensor.TYPE_GRAVITY
+ )
+
+ self.values = [None, None, None]
+
+ def enable(self):
+ self.SensorManager.registerListener(
+ self,
+ self.sensor,
+ SensorManager.SENSOR_DELAY_NORMAL
+ )
+
+ def disable(self):
+ self.SensorManager.unregisterListener(self, self.sensor)
+
+ @java_method('(Landroid/hardware/SensorEvent;)V')
+ def onSensorChanged(self, event):
+ self.values = event.values[:3]
+
+ @java_method('(Landroid/hardware/Sensor;I)V')
+ def onAccuracyChanged(self, sensor, accuracy):
+ pass
+
+
+class AndroidGravity(Gravity):
+
+ def __init__(self):
+ super().__init__()
+ self.state = False
+
+ def _enable(self):
+ if not self.state:
+ self.listener = GravitySensorListener()
+ self.listener.enable()
+ self.state = True
+
+ def _disable(self):
+ if self.state:
+ self.state = False
+ self.listener.disable()
+ del self.listener
+
+ def _get_gravity(self):
+ if self.state:
+ return tuple(self.listener.values)
+ else:
+ return (None, None, None)
+
+ def __del__(self):
+ if self.state:
+ self._disable()
+ super().__del__()
+
+
+def instance():
+ return AndroidGravity()
diff --git a/sbapp/plyer/platforms/android/gyroscope.py b/sbapp/plyer/platforms/android/gyroscope.py
new file mode 100644
index 00000000..d6382b15
--- /dev/null
+++ b/sbapp/plyer/platforms/android/gyroscope.py
@@ -0,0 +1,119 @@
+'''
+Android Gyroscope
+-----------------
+'''
+
+from plyer.facades import Gyroscope
+from jnius import PythonJavaClass, java_method, autoclass, cast
+from plyer.platforms.android import activity
+
+Context = autoclass('android.content.Context')
+Sensor = autoclass('android.hardware.Sensor')
+SensorManager = autoclass('android.hardware.SensorManager')
+
+
+class GyroscopeSensorListener(PythonJavaClass):
+ __javainterfaces__ = ['android/hardware/SensorEventListener']
+
+ def __init__(self):
+ super().__init__()
+ self.SensorManager = cast(
+ 'android.hardware.SensorManager',
+ activity.getSystemService(Context.SENSOR_SERVICE)
+ )
+ self.sensor = self.SensorManager.getDefaultSensor(
+ Sensor.TYPE_GYROSCOPE
+ )
+
+ self.values = [None, None, None]
+
+ def enable(self):
+ self.SensorManager.registerListener(
+ self, self.sensor,
+ SensorManager.SENSOR_DELAY_NORMAL
+ )
+
+ def disable(self):
+ self.SensorManager.unregisterListener(self, self.sensor)
+
+ @java_method('(Landroid/hardware/SensorEvent;)V')
+ def onSensorChanged(self, event):
+ self.values = event.values[:3]
+
+ @java_method('(Landroid/hardware/Sensor;I)V')
+ def onAccuracyChanged(self, sensor, accuracy):
+ # Maybe, do something in future?
+ pass
+
+
+class GyroUncalibratedSensorListener(PythonJavaClass):
+ __javainterfaces__ = ['android/hardware/SensorEventListener']
+
+ def __init__(self):
+ super().__init__()
+ service = activity.getSystemService(Context.SENSOR_SERVICE)
+ self.SensorManager = cast('android.hardware.SensorManager', service)
+
+ self.sensor = self.SensorManager.getDefaultSensor(
+ Sensor.TYPE_GYROSCOPE_UNCALIBRATED)
+ self.values = [None, None, None, None, None, None]
+
+ def enable(self):
+ self.SensorManager.registerListener(
+ self, self.sensor,
+ SensorManager.SENSOR_DELAY_NORMAL
+ )
+
+ def disable(self):
+ self.SensorManager.unregisterListener(self, self.sensor)
+
+ @java_method('(Landroid/hardware/SensorEvent;)V')
+ def onSensorChanged(self, event):
+ self.values = event.values[:6]
+
+ @java_method('(Landroid/hardware/Sensor;I)V')
+ def onAccuracyChanged(self, sensor, accuracy):
+ pass
+
+
+class AndroidGyroscope(Gyroscope):
+ def __init__(self):
+ super().__init__()
+ self.bState = False
+
+ def _enable(self):
+ if (not self.bState):
+ self.listenerg = GyroscopeSensorListener()
+ self.listenergu = GyroUncalibratedSensorListener()
+ self.listenerg.enable()
+ self.listenergu.enable()
+ self.bState = True
+
+ def _disable(self):
+ if (self.bState):
+ self.bState = False
+ self.listenerg.disable()
+ self.listenergu.disable()
+ del self.listenerg
+ del self.listenergu
+
+ def _get_orientation(self):
+ if (self.bState):
+ return tuple(self.listenerg.values)
+ else:
+ return (None, None, None)
+
+ def _get_rotation_uncalib(self):
+ if (self.bState):
+ return tuple(self.listenergu.values)
+ else:
+ return (None, None, None, None, None, None)
+
+ def __del__(self):
+ if(self.bState):
+ self._disable()
+ super().__del__()
+
+
+def instance():
+ return AndroidGyroscope()
diff --git a/sbapp/plyer/platforms/android/humidity.py b/sbapp/plyer/platforms/android/humidity.py
new file mode 100644
index 00000000..78e17ac2
--- /dev/null
+++ b/sbapp/plyer/platforms/android/humidity.py
@@ -0,0 +1,107 @@
+from jnius import autoclass
+from jnius import cast
+from jnius import java_method
+from jnius import PythonJavaClass
+from math import exp
+from plyer.facades import Humidity
+from plyer.platforms.android import activity
+
+ActivityInfo = autoclass('android.content.pm.ActivityInfo')
+Context = autoclass('android.content.Context')
+Sensor = autoclass('android.hardware.Sensor')
+SensorManager = autoclass('android.hardware.SensorManager')
+
+
+class RelativeHumiditySensorListener(PythonJavaClass):
+ __javainterfaces__ = ['android/hardware/SensorEventListener']
+
+ def __init__(self):
+ super().__init__()
+ service = activity.getSystemService(Context.SENSOR_SERVICE)
+ self.SensorManager = cast('android.hardware.SensorManager', service)
+
+ self.sensor = self.SensorManager.getDefaultSensor(
+ Sensor.TYPE_RELATIVE_HUMIDITY)
+ self.value = None
+
+ def enable(self):
+ self.SensorManager.registerListener(self, self.sensor,
+ SensorManager.SENSOR_DELAY_NORMAL)
+
+ def disable(self):
+ self.SensorManager.unregisterListener(self, self.sensor)
+
+ @java_method('(Landroid/hardware/SensorEvent;)V')
+ def onSensorChanged(self, event):
+ self.value = event.values[0]
+
+ @java_method('(Landroid/hardware/Sensor;I)V')
+ def onAccuracyChanged(self, sensor, accuracy):
+ pass
+
+
+class AmbientTemperatureSensorListener(PythonJavaClass):
+ __javainterfaces__ = ['android/hardware/SensorEventListener']
+
+ def __init__(self):
+ super().__init__()
+ service = activity.getSystemService(Context.SENSOR_SERVICE)
+ self.SensorManager = cast('android.hardware.SensorManager', service)
+
+ self.sensor = self.SensorManager.getDefaultSensor(
+ Sensor.TYPE_AMBIENT_TEMPERATURE)
+ self.value = None
+
+ def enable(self):
+ self.SensorManager.registerListener(self, self.sensor,
+ SensorManager.SENSOR_DELAY_NORMAL)
+
+ def disable(self):
+ self.SensorManager.unregisterListener(self, self.sensor)
+
+ @java_method('(Landroid/hardware/SensorEvent;)V')
+ def onSensorChanged(self, event):
+ self.value = event.values[0]
+
+ @java_method('(Landroid/hardware/Sensor;I)V')
+ def onAccuracyChanged(self, sensor, accuracy):
+ pass
+
+
+class AndroidHumidity(Humidity):
+
+ def __init__(self):
+ self.state = False
+
+ def _get_humidity(self):
+ if self.state:
+ m = 17.62
+ Tn = 243.12
+ Ta = 216.7
+ Rh = self.listener_r.value
+ Tc = self.listener_a.value
+ A = 6.112
+ K = 273.15
+ humidity = (Ta * (Rh / 100) * A * exp(m * Tc / (Tn + Tc))
+ / (K + Tc))
+ return humidity
+
+ def _enable(self):
+ if not self.state:
+ self.listener_r = RelativeHumiditySensorListener()
+ self.listener_a = AmbientTemperatureSensorListener()
+ self.listener_r.enable()
+ self.listener_a.enable()
+ self.state = True
+
+ def _disable(self):
+ if self.state:
+ self.listener_r.disable()
+ self.listener_a.disable()
+ self.state = False
+ delattr(self, 'listener_r')
+ delattr(self, 'listener_a')
+
+
+def instance():
+ return AndroidHumidity()
diff --git a/sbapp/plyer/platforms/android/irblaster.py b/sbapp/plyer/platforms/android/irblaster.py
new file mode 100644
index 00000000..06d311ba
--- /dev/null
+++ b/sbapp/plyer/platforms/android/irblaster.py
@@ -0,0 +1,55 @@
+from jnius import autoclass
+
+from plyer.facades import IrBlaster
+from plyer.platforms.android import activity, SDK_INT, ANDROID_VERSION
+
+if SDK_INT >= 19:
+ Context = autoclass('android.content.Context')
+ ir_manager = activity.getSystemService(Context.CONSUMER_IR_SERVICE)
+else:
+ ir_manager = None
+
+
+class AndroidIrBlaster(IrBlaster):
+ def _exists(self):
+ if ir_manager and ir_manager.hasIrEmitter():
+ return True
+ return False
+
+ @property
+ def multiply_pulse(self):
+ '''Android 4.4.3+ uses microseconds instead of period counts
+ '''
+ return not (SDK_INT == 19
+ and int(str(ANDROID_VERSION.RELEASE).rsplit('.', 1)[-1])
+ < 3)
+
+ def _get_frequencies(self):
+ if not ir_manager:
+ return None
+
+ if hasattr(self, '_frequencies'):
+ return self._frequencies
+
+ ir_frequencies = ir_manager.getCarrierFrequencies()
+ if not ir_frequencies:
+ return []
+
+ frequencies = []
+ for freqrange in ir_frequencies:
+ freq = (freqrange.getMinFrequency(), freqrange.getMaxFrequency())
+ frequencies.append(freq)
+
+ self._frequencies = frequencies
+ return frequencies
+
+ def _transmit(self, frequency, pattern, mode):
+ if self.multiply_pulse and mode == 'period':
+ pattern = self.periods_to_microseconds(frequency, pattern)
+ elif not self.multiply_pulse and mode == 'microseconds':
+ pattern = self.microseconds_to_periods(frequency, pattern)
+ ir_manager.transmit(frequency, pattern)
+
+
+def instance():
+ return AndroidIrBlaster()
diff --git a/sbapp/plyer/platforms/android/keystore.py b/sbapp/plyer/platforms/android/keystore.py
new file mode 100644
index 00000000..0339948c
--- /dev/null
+++ b/sbapp/plyer/platforms/android/keystore.py
@@ -0,0 +1,25 @@
+from plyer.facades import Keystore
+from plyer.platforms.android import activity
+
+
+class AndroidKeystore(Keystore):
+
+ def _set_key(self, servicename, key, value, **kwargs):
+ mode = kwargs.get("mode", 0)
+ settings = activity.getSharedPreferences(servicename, mode)
+ editor = settings.edit()
+ editor.putString(key, value)
+ editor.commit()
+
+ def _get_key(self, servicename, key, **kwargs):
+ mode = kwargs.get("mode", 0)
+ default = kwargs.get("default", "__None")
+ settings = activity.getSharedPreferences(servicename, mode)
+ ret = settings.getString(key, default)
+ if ret == "__None":
+ ret = None
+ return ret
+
+
+def instance():
+ return AndroidKeystore()
diff --git a/sbapp/plyer/platforms/android/light.py b/sbapp/plyer/platforms/android/light.py
new file mode 100644
index 00000000..13dd3944
--- /dev/null
+++ b/sbapp/plyer/platforms/android/light.py
@@ -0,0 +1,63 @@
+from jnius import autoclass
+from jnius import cast
+from jnius import java_method
+from jnius import PythonJavaClass
+
+from plyer.facades import Light
+from plyer.platforms.android import activity
+
+Context = autoclass('android.content.Context')
+Sensor = autoclass('android.hardware.Sensor')
+SensorManager = autoclass('android.hardware.SensorManager')
+
+
+class LightSensorListener(PythonJavaClass):
+ __javainterfaces__ = ['android/hardware/SensorEventListener']
+
+ def __init__(self):
+ super().__init__()
+ service = activity.getSystemService(Context.SENSOR_SERVICE)
+ self.SensorManager = cast('android.hardware.SensorManager', service)
+ self.sensor = self.SensorManager.getDefaultSensor(Sensor.TYPE_LIGHT)
+ self.value = None
+
+ def enable(self):
+ self.SensorManager.registerListener(
+ self, self.sensor,
+ SensorManager.SENSOR_DELAY_NORMAL
+ )
+
+ def disable(self):
+ self.SensorManager.unregisterListener(self, self.sensor)
+
+ @java_method('(Landroid/hardware/SensorEvent;)V')
+ def onSensorChanged(self, event):
+ self.value = event.values[0]
+
+ @java_method('(Landroid/hardware/Sensor;I)V')
+ def onAccuracyChanged(self, sensor, accuracy):
+ pass
+
+
+class AndroidLight(Light):
+
+ listener = None
+
+ def _get_illumination(self):
+ if self.listener and self.listener.value:
+ light = self.listener.value
+ return light
+
+ def _enable(self):
+ if not self.listener:
+ self.listener = LightSensorListener()
+ self.listener.enable()
+
+ def _disable(self):
+ if self.listener:
+ self.listener.disable()
+ delattr(self, 'listener')
+
+
+def instance():
+ return AndroidLight()
diff --git a/sbapp/plyer/platforms/android/notification.py b/sbapp/plyer/platforms/android/notification.py
new file mode 100644
index 00000000..24a567da
--- /dev/null
+++ b/sbapp/plyer/platforms/android/notification.py
@@ -0,0 +1,208 @@
+'''
+Module of Android API for plyer.notification.
+
+.. versionadded:: 1.0.0
+
+.. versionchanged:: 1.4.0
+ Fixed notifications not displaying due to missing NotificationChannel
+ required by Android Oreo 8.0+ (API 26+).
+
+.. versionchanged:: 1.4.0
+ Added simple toaster notification.
+
+.. versionchanged:: 1.4.0
+ Fixed notifications not displaying big icons properly.
+ Added option for custom big icon via `icon`.
+'''
+
+from android import python_act
+from android.runnable import run_on_ui_thread
+from jnius import autoclass, cast
+
+from plyer.facades import Notification
+from plyer.platforms.android import activity, SDK_INT
+
+AndroidString = autoclass('java.lang.String')
+Context = autoclass('android.content.Context')
+NotificationBuilder = autoclass('android.app.Notification$Builder')
+NotificationManager = autoclass('android.app.NotificationManager')
+PendingIntent = autoclass('android.app.PendingIntent')
+Intent = autoclass('android.content.Intent')
+Toast = autoclass('android.widget.Toast')
+BitmapFactory = autoclass('android.graphics.BitmapFactory')
+Icon = autoclass("android.graphics.drawable.Icon")
+
+
+class AndroidNotification(Notification):
+ '''
+ Implementation of Android notification API.
+
+ .. versionadded:: 1.0.0
+ '''
+
+ def __init__(self):
+ package_name = activity.getPackageName()
+ self._ns = None
+ self._channel_id = package_name
+
+ pm = activity.getPackageManager()
+ info = pm.getActivityInfo(activity.getComponentName(), 0)
+ if info.icon == 0:
+ # Take the application icon instead.
+ info = pm.getApplicationInfo(package_name, 0)
+
+ self._app_icon = info.icon
+
+ def _get_notification_service(self):
+ if not self._ns:
+ self._ns = cast(NotificationManager, activity.getSystemService(
+ Context.NOTIFICATION_SERVICE
+ ))
+ return self._ns
+
+ def _build_notification_channel(self, name):
+ '''
+ Create a NotificationChannel using channel id of the application
+ package name (com.xyz, org.xyz, ...) and channel name same as the
+ provided notification title if the API is high enough, otherwise
+ do nothing.
+
+ .. versionadded:: 1.4.0
+ '''
+
+ if SDK_INT < 26:
+ return
+
+ channel = autoclass('android.app.NotificationChannel')
+
+ app_channel = channel(
+ self._channel_id, name, NotificationManager.IMPORTANCE_DEFAULT
+ )
+ self._get_notification_service().createNotificationChannel(
+ app_channel
+ )
+ return app_channel
+
+ @run_on_ui_thread
+ def _toast(self, message):
+ '''
+ Display a popup-like small notification at the bottom of the screen.
+
+ .. versionadded:: 1.4.0
+ '''
+ Toast.makeText(
+ activity,
+ cast('java.lang.CharSequence', AndroidString(message)),
+ Toast.LENGTH_LONG
+ ).show()
+
+ def _set_icons(self, notification, icon=None, notification_icon=None):
+ '''
+ Set the small application icon displayed at the top panel together with
+ WiFi, battery percentage and time and the big optional icon (preferably
+ PNG format with transparent parts) displayed directly in the
+ notification body.
+
+ .. versionadded:: 1.4.0
+ '''
+ if notification_icon == None:
+ app_icon = self._app_icon
+ else:
+ notification_icon_bitmap = BitmapFactory.decodeFile(notification_icon)
+ app_icon = Icon.createWithBitmap(notification_icon_bitmap)
+
+ notification.setSmallIcon(app_icon)
+
+ bitmap_icon = app_icon
+ if icon is not None:
+ bitmap_icon = BitmapFactory.decodeFile(icon)
+ notification.setLargeIcon(bitmap_icon)
+ elif icon == '':
+ # we don't want the big icon set,
+ # only the small one in the top panel
+ pass
+ else:
+ bitmap_icon = BitmapFactory.decodeResource(
+ python_act.getResources(), app_icon
+ )
+ notification.setLargeIcon(bitmap_icon)
+
+ def _build_notification(self, title):
+ '''
+ .. versionadded:: 1.4.0
+ '''
+ if SDK_INT < 26:
+ noti = NotificationBuilder(activity)
+ else:
+ self._channel = self._build_notification_channel(title)
+ noti = NotificationBuilder(activity, self._channel_id)
+ return noti
+
+ @staticmethod
+ def _set_open_behavior(notification):
+ '''
+ Open the source application when user opens the notification.
+
+ .. versionadded:: 1.4.0
+ '''
+
+ # create Intent that navigates back to the application
+ app_context = activity.getApplication().getApplicationContext()
+ notification_intent = Intent(app_context, python_act)
+
+ # set flags to run our application Activity
+ notification_intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
+ notification_intent.setAction(Intent.ACTION_MAIN)
+ notification_intent.addCategory(Intent.CATEGORY_LAUNCHER)
+
+ # get our application Activity
+ pending_intent = PendingIntent.getActivity(
+ app_context, 0, notification_intent, 0
+ )
+
+ notification.setContentIntent(pending_intent)
+ notification.setAutoCancel(True)
+
+ def _open_notification(self, notification):
+ if SDK_INT >= 16:
+ notification = notification.build()
+ else:
+ notification = notification.getNotification()
+
+ self._get_notification_service().notify(0, notification)
+
+ def _notify(self, **kwargs):
+ noti = None
+ message = kwargs.get('message').encode('utf-8')
+ ticker = kwargs.get('ticker').encode('utf-8')
+ title = AndroidString(
+ kwargs.get('title', '').encode('utf-8')
+ )
+ icon = kwargs.get('app_icon')
+ notification_icon = kwargs.get('notification_icon')
+
+ # decide whether toast only or proper notification
+ if kwargs.get('toast'):
+ self._toast(message)
+ return
+ else:
+ noti = self._build_notification(title)
+
+ # set basic properties for notification
+ noti.setContentTitle(title)
+ noti.setContentText(AndroidString(message))
+ noti.setTicker(AndroidString(ticker))
+
+ # set additional flags for notification
+ self._set_icons(noti, icon=icon, notification_icon=notification_icon)
+ self._set_open_behavior(noti)
+
+ # launch
+ self._open_notification(noti)
+
+
+def instance():
+ '''
+ Instance for facade proxy.
+ '''
+ return AndroidNotification()
diff --git a/sbapp/plyer/platforms/android/orientation.py b/sbapp/plyer/platforms/android/orientation.py
new file mode 100644
index 00000000..5f10958f
--- /dev/null
+++ b/sbapp/plyer/platforms/android/orientation.py
@@ -0,0 +1,43 @@
+from jnius import autoclass
+from plyer.platforms.android import activity
+from plyer.facades import Orientation
+
+ActivityInfo = autoclass('android.content.pm.ActivityInfo')
+
+
+class AndroidOrientation(Orientation):
+
+ def _set_landscape(self, **kwargs):
+ reverse = kwargs.get('reverse')
+ if reverse:
+ activity.setRequestedOrientation(
+ ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE)
+ else:
+ activity.setRequestedOrientation(
+ ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
+
+ def _set_portrait(self, **kwargs):
+ reverse = kwargs.get('reverse')
+ if reverse:
+ activity.setRequestedOrientation(
+ ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT)
+ else:
+ activity.setRequestedOrientation(
+ ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
+
+ def _set_sensor(self, **kwargs):
+ mode = kwargs.get('mode')
+
+ if mode == 'any':
+ activity.setRequestedOrientation(
+ ActivityInfo.SCREEN_ORIENTATION_SENSOR)
+ elif mode == 'landscape':
+ activity.setRequestedOrientation(
+ ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE)
+ elif mode == 'portrait':
+ activity.setRequestedOrientation(
+ ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT)
+
+
+def instance():
+ return AndroidOrientation()
diff --git a/sbapp/plyer/platforms/android/proximity.py b/sbapp/plyer/platforms/android/proximity.py
new file mode 100644
index 00000000..b33ecda2
--- /dev/null
+++ b/sbapp/plyer/platforms/android/proximity.py
@@ -0,0 +1,69 @@
+from jnius import autoclass
+from jnius import cast
+from jnius import java_method
+from jnius import PythonJavaClass
+
+from plyer.platforms.android import activity
+from plyer.facades import Proximity
+
+ActivityInfo = autoclass('android.content.pm.ActivityInfo')
+Context = autoclass('android.content.Context')
+Sensor = autoclass('android.hardware.Sensor')
+SensorManager = autoclass('android.hardware.SensorManager')
+
+
+class ProximitySensorListener(PythonJavaClass):
+ __javainterfaces__ = ['android/hardware/SensorEventListener']
+
+ def __init__(self):
+ super().__init__()
+ service = activity.getSystemService(Context.SENSOR_SERVICE)
+ self.SensorManager = cast('android.hardware.SensorManager', service)
+
+ self.sensor = self.SensorManager.getDefaultSensor(
+ Sensor.TYPE_PROXIMITY)
+ self.value = None
+
+ def enable(self):
+ self.SensorManager.registerListener(
+ self, self.sensor,
+ SensorManager.SENSOR_DELAY_NORMAL
+ )
+
+ def disable(self):
+ self.SensorManager.unregisterListener(self, self.sensor)
+
+ @java_method('(Landroid/hardware/SensorEvent;)V')
+ def onSensorChanged(self, event):
+ self.value = event.values[0]
+
+ @java_method('(Landroid/hardware/Sensor;I)V')
+ def onAccuracyChanged(self, sensor, accuracy):
+ pass
+
+
+class AndroidProximity(Proximity):
+
+ listener = None
+
+ def _enable(self, **kwargs):
+ if not self.listener:
+ self.listener = ProximitySensorListener()
+ self.listener.enable()
+
+ def _disable(self, **kwargs):
+ if self.listener:
+ self.listener.disable()
+ delattr(self, 'listener')
+
+ def _get_proximity(self):
+ if self.listener:
+ value = self.listener.value
+ # value is 0.0 when proxime sensor is covered. In other case
+ # value is 5.0 because in smartphone, optical proximity sensors
+ # are used.
+ return value < 5.0
+
+
+def instance():
+ return AndroidProximity()
diff --git a/sbapp/plyer/platforms/android/sms.py b/sbapp/plyer/platforms/android/sms.py
new file mode 100644
index 00000000..8650968c
--- /dev/null
+++ b/sbapp/plyer/platforms/android/sms.py
@@ -0,0 +1,25 @@
+'''
+Android SMS
+-----------
+'''
+
+from jnius import autoclass
+from plyer.facades import Sms
+
+SmsManager = autoclass('android.telephony.SmsManager')
+
+
+class AndroidSms(Sms):
+
+ def _send(self, **kwargs):
+ sms = SmsManager.getDefault()
+
+ recipient = kwargs.get('recipient')
+ message = kwargs.get('message')
+
+ if sms:
+ sms.sendTextMessage(recipient, None, message, None, None)
+
+
+def instance():
+ return AndroidSms()
diff --git a/sbapp/plyer/platforms/android/spatialorientation.py b/sbapp/plyer/platforms/android/spatialorientation.py
new file mode 100644
index 00000000..e0df1d33
--- /dev/null
+++ b/sbapp/plyer/platforms/android/spatialorientation.py
@@ -0,0 +1,118 @@
+from jnius import autoclass
+from jnius import cast
+from jnius import java_method
+from jnius import PythonJavaClass
+from plyer.platforms.android import activity
+from plyer.facades import SpatialOrientation
+
+Context = autoclass('android.content.Context')
+Sensor = autoclass('android.hardware.Sensor')
+SensorManager = autoclass('android.hardware.SensorManager')
+
+
+class AccelerometerSensorListener(PythonJavaClass):
+ __javainterfaces__ = ['android/hardware/SensorEventListener']
+
+ def __init__(self):
+ super().__init__()
+ self.SensorManager = cast(
+ 'android.hardware.SensorManager',
+ activity.getSystemService(Context.SENSOR_SERVICE)
+ )
+ self.sensor = self.SensorManager.getDefaultSensor(
+ Sensor.TYPE_ACCELEROMETER
+ )
+ self.values = [None, None, None]
+
+ def enable(self):
+ self.SensorManager.registerListener(
+ self, self.sensor,
+ SensorManager.SENSOR_DELAY_NORMAL
+ )
+
+ def disable(self):
+ self.SensorManager.unregisterListener(self, self.sensor)
+
+ @java_method('(Landroid/hardware/SensorEvent;)V')
+ def onSensorChanged(self, event):
+ self.values = event.values[:3]
+
+ @java_method('(Landroid/hardware/Sensor;I)V')
+ def onAccuracyChanged(self, sensor, accuracy):
+ pass
+
+
+class MagnetometerSensorListener(PythonJavaClass):
+ __javainterfaces__ = ['android/hardware/SensorEventListener']
+
+ def __init__(self):
+ super().__init__()
+ service = activity.getSystemService(Context.SENSOR_SERVICE)
+ self.SensorManager = cast('android.hardware.SensorManager', service)
+
+ self.sensor = self.SensorManager.getDefaultSensor(
+ Sensor.TYPE_MAGNETIC_FIELD)
+ self.values = [None, None, None]
+
+ def enable(self):
+ self.SensorManager.registerListener(
+ self, self.sensor,
+ SensorManager.SENSOR_DELAY_NORMAL
+ )
+
+ def disable(self):
+ self.SensorManager.unregisterListener(self, self.sensor)
+
+ @java_method('(Landroid/hardware/SensorEvent;)V')
+ def onSensorChanged(self, event):
+ self.values = event.values[:3]
+
+ @java_method('(Landroid/hardware/Sensor;I)V')
+ def onAccuracyChanged(self, sensor, accuracy):
+ pass
+
+
+class AndroidSpOrientation(SpatialOrientation):
+
+ def __init__(self):
+ self.state = False
+
+ def _get_orientation(self):
+ if self.state:
+ rotation = [0] * 9
+ inclination = [0] * 9
+ gravity = []
+ geomagnetic = []
+ gravity = self.listener_a.values
+ geomagnetic = self.listener_m.values
+ if gravity[0] is not None and geomagnetic[0] is not None:
+ ff_state = SensorManager.getRotationMatrix(
+ rotation, inclination,
+ gravity, geomagnetic
+ )
+ if ff_state:
+ values = [0, 0, 0]
+ values = SensorManager.getOrientation(
+ rotation, values
+ )
+ return values
+
+ def _enable_listener(self, **kwargs):
+ if not self.state:
+ self.listener_a = AccelerometerSensorListener()
+ self.listener_m = MagnetometerSensorListener()
+ self.listener_a.enable()
+ self.listener_m.enable()
+ self.state = True
+
+ def _disable_listener(self, **kwargs):
+ if self.state:
+ self.listener_a.disable()
+ self.listener_m.disable()
+ self.state = False
+ delattr(self, 'listener_a')
+ delattr(self, 'listener_m')
+
+
+def instance():
+ return AndroidSpOrientation()
diff --git a/sbapp/plyer/platforms/android/storagepath.py b/sbapp/plyer/platforms/android/storagepath.py
new file mode 100755
index 00000000..788e3fce
--- /dev/null
+++ b/sbapp/plyer/platforms/android/storagepath.py
@@ -0,0 +1,69 @@
+'''
+Android Storage Path
+--------------------
+'''
+
+from os import listdir, access, R_OK
+from os.path import join
+from plyer.facades import StoragePath
+from jnius import autoclass
+from android import mActivity
+
+Environment = autoclass('android.os.Environment')
+Context = autoclass('android.content.Context')
+
+
+class AndroidStoragePath(StoragePath):
+
+ def _get_home_dir(self):
+ return Environment.getDataDirectory().getAbsolutePath()
+
+ def _get_external_storage_dir(self):
+ return Environment.getExternalStorageDirectory().getAbsolutePath()
+
+ def _get_sdcard_dir(self):
+ '''
+ .. versionadded:: 1.4.0
+ '''
+ # folder in /storage/ that is readable
+ # and is not internal SD card
+ path = None
+ for folder in listdir('/storage'):
+ folder = join('/storage', folder)
+ if folder in self._get_external_storage_dir():
+ continue
+ if not access(folder, R_OK):
+ continue
+ path = folder
+ break
+ return path
+
+ def _get_root_dir(self):
+ return Environment.getRootDirectory().getAbsolutePath()
+
+ def _get_documents_dir(self):
+ return Environment.getExternalStoragePublicDirectory(
+ Environment.DIRECTORY_DOCUMENTS).getAbsolutePath()
+
+ def _get_downloads_dir(self):
+ return Environment.getExternalStoragePublicDirectory(
+ Environment.DIRECTORY_DOWNLOADS).getAbsolutePath()
+
+ def _get_videos_dir(self):
+ return Environment.getExternalStoragePublicDirectory(
+ Environment.DIRECTORY_MOVIES).getAbsolutePath()
+
+ def _get_music_dir(self):
+ return Environment.getExternalStoragePublicDirectory(
+ Environment.DIRECTORY_MUSIC).getAbsolutePath()
+
+ def _get_pictures_dir(self):
+ return Environment.getExternalStoragePublicDirectory(
+ Environment.DIRECTORY_PICTURES).getAbsolutePath()
+
+ def _get_application_dir(self):
+ return mActivity.getFilesDir().getParentFile().getParent()
+
+
+def instance():
+ return AndroidStoragePath()
diff --git a/sbapp/plyer/platforms/android/stt.py b/sbapp/plyer/platforms/android/stt.py
new file mode 100644
index 00000000..51681e04
--- /dev/null
+++ b/sbapp/plyer/platforms/android/stt.py
@@ -0,0 +1,252 @@
+from android.runnable import run_on_ui_thread
+
+from jnius import autoclass
+from jnius import java_method
+from jnius import PythonJavaClass
+
+from plyer.facades import STT
+from plyer.platforms.android import activity
+
+ArrayList = autoclass('java.util.ArrayList')
+Bundle = autoclass('android.os.Bundle')
+Context = autoclass('android.content.Context')
+Intent = autoclass('android.content.Intent')
+RecognizerIntent = autoclass('android.speech.RecognizerIntent')
+RecognitionListener = autoclass('android.speech.RecognitionListener')
+SpeechRecognizer = autoclass('android.speech.SpeechRecognizer')
+
+SpeechResults = SpeechRecognizer.RESULTS_RECOGNITION
+
+
+class SpeechListener(PythonJavaClass):
+ __javainterfaces__ = ['android/speech/RecognitionListener']
+
+ # class variables because PythonJavaClass class failed
+ # to see them later in getters and setters
+ _error_callback = None
+ _result_callback = None
+ _partial_result_callback = None
+ _volume_callback = None
+
+ def __init__(self):
+ super().__init__()
+
+ # overwrite class variables in the object
+ self._error_callback = None
+ self._result_callback = None
+ self._partial_result_callback = None
+ self._volume_callback = None
+
+ # error handling
+ @property
+ def error_callback(self):
+ return self._error_callback
+
+ @error_callback.setter
+ def error_callback(self, callback):
+ '''
+ Set error callback. It is called when error occurs.
+
+ :param callback: function with one parameter for error message
+ '''
+
+ self._error_callback = callback
+
+ # result handling
+ @property
+ def result_callback(self):
+ return self._result_callback
+
+ @result_callback.setter
+ def result_callback(self, callback):
+ '''
+ Set result callback. It is called when results are received.
+
+ :param callback: function with one parameter for lists of strings
+ '''
+
+ self._result_callback = callback
+
+ @property
+ def partial_result_callback(self):
+ return self._partial_result_callback
+
+ @partial_result_callback.setter
+ def partial_result_callback(self, callback):
+ '''
+ Set partial result callback. It is called when partial results are
+ received while the listener is still in listening mode.
+
+ :param callback: function with one parameter for lists of strings
+ '''
+
+ self._partial_result_callback = callback
+
+ # voice changes handling
+ @property
+ def volume_callback(self):
+ return self._volume_callback
+
+ @volume_callback.setter
+ def volume_callback(self, callback):
+ '''
+ Set volume voice callback.
+
+ It is called when loudness of the voice changes.
+
+ :param callback: function with one parameter for volume RMS dB (float).
+ '''
+ self._volume_callback = callback
+
+ # Implementation Java Interfaces
+ @java_method('()V')
+ def onBeginningOfSpeech(self):
+ pass
+
+ @java_method('([B)V')
+ def onBufferReceived(self, buffer):
+ pass
+
+ @java_method('()V')
+ def onEndOfSpeech(self):
+ pass
+
+ @java_method('(I)V')
+ def onError(self, error):
+ msg = ''
+ if error == SpeechRecognizer.ERROR_AUDIO:
+ msg = 'audio'
+ if error == SpeechRecognizer.ERROR_CLIENT:
+ msg = 'client'
+ if error == SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS:
+ msg = 'insufficient_permissions'
+ if error == SpeechRecognizer.ERROR_NETWORK:
+ msg = 'network'
+ if error == SpeechRecognizer.ERROR_NETWORK_TIMEOUT:
+ msg = 'network_timeout'
+ if error == SpeechRecognizer.ERROR_NO_MATCH:
+ msg = 'no_match'
+ if error == SpeechRecognizer.ERROR_RECOGNIZER_BUSY:
+ msg = 'recognizer_busy'
+ if error == SpeechRecognizer.ERROR_SERVER:
+ msg = 'server'
+ if error == SpeechRecognizer.ERROR_SPEECH_TIMEOUT:
+ msg = 'speech_timeout'
+
+ if msg and self.error_callback:
+ self.error_callback('error:' + msg)
+
+ @java_method('(ILandroid/os/Bundle;)V')
+ def onEvent(self, event_type, params):
+ pass
+
+ @java_method('(Landroid/os/Bundle;)V')
+ def onPartialResults(self, results):
+ texts = []
+ matches = results.getStringArrayList(SpeechResults)
+ for match in matches.toArray():
+ if isinstance(match, bytes):
+ match = match.decode('utf-8')
+ texts.append(match)
+
+ if texts and self.partial_result_callback:
+ self.partial_result_callback(texts)
+
+ @java_method('(Landroid/os/Bundle;)V')
+ def onReadyForSpeech(self, params):
+ pass
+
+ @java_method('(Landroid/os/Bundle;)V')
+ def onResults(self, results):
+ texts = []
+ matches = results.getStringArrayList(SpeechResults)
+ for match in matches.toArray():
+ if isinstance(match, bytes):
+ match = match.decode('utf-8')
+ texts.append(match)
+
+ if texts and self.result_callback:
+ self.result_callback(texts)
+
+ @java_method('(F)V')
+ def onRmsChanged(self, rmsdB):
+ if self.volume_callback:
+ self.volume_callback(rmsdB)
+
+
+class AndroidSpeech(STT):
+ '''
+ Android Speech Implementation.
+
+ Android class `SpeechRecognizer`'s listening deactivates automatically.
+
+ Class methods `_on_error()`, `_on_result()` listeners. You can find
+ documentation here:
+ https://developer.android.com/reference/android/speech/RecognitionListener
+ '''
+
+ def _on_error(self, msg):
+ self.errors.append(msg)
+ self.stop()
+
+ def _on_result(self, messages):
+ self.results.extend(messages)
+ self.stop()
+
+ def _on_partial(self, messages):
+ self.partial_results.extend(messages)
+
+ @run_on_ui_thread
+ def _start(self):
+ intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)
+ intent.putExtra(
+ RecognizerIntent.EXTRA_CALLING_PACKAGE,
+ activity.getPackageName()
+ )
+
+ # language preferences
+ intent.putExtra(
+ RecognizerIntent.EXTRA_LANGUAGE_PREFERENCE, self.language
+ )
+ intent.putExtra(
+ RecognizerIntent.EXTRA_LANGUAGE_MODEL,
+ RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH
+ )
+
+ # results settings
+ intent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 1000)
+ intent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, True)
+ if self.prefer_offline:
+ intent.putExtra(RecognizerIntent.EXTRA_PREFER_OFFLINE, True)
+
+ # listener and callbacks
+ listener = SpeechListener()
+ listener.error_callback = self._on_error
+ listener.result_callback = self._on_result
+ listener.partial_result_callback = self._on_partial
+
+ # create recognizer and start
+ self.speech = SpeechRecognizer.createSpeechRecognizer(activity)
+ self.speech.setRecognitionListener(listener)
+ self.speech.startListening(intent)
+
+ @run_on_ui_thread
+ def _stop(self):
+ if not self.speech:
+ return
+
+ # stop listening
+ self.speech.stopListening()
+
+ # free object
+ self.speech.destroy()
+ self.speech = None
+
+ def _exist(self):
+ return bool(
+ SpeechRecognizer.isRecognitionAvailable(activity)
+ )
+
+
+def instance():
+ return AndroidSpeech()
diff --git a/sbapp/plyer/platforms/android/temperature.py b/sbapp/plyer/platforms/android/temperature.py
new file mode 100644
index 00000000..fb053639
--- /dev/null
+++ b/sbapp/plyer/platforms/android/temperature.py
@@ -0,0 +1,66 @@
+from jnius import autoclass
+from jnius import cast
+from jnius import java_method
+from jnius import PythonJavaClass
+
+from plyer.facades import Temperature
+from plyer.platforms.android import activity
+
+ActivityInfo = autoclass('android.content.pm.ActivityInfo')
+Context = autoclass('android.content.Context')
+Sensor = autoclass('android.hardware.Sensor')
+SensorManager = autoclass('android.hardware.SensorManager')
+
+
+class TemperatureSensorListener(PythonJavaClass):
+ __javainterfaces__ = ['android/hardware/SensorEventListener']
+
+ def __init__(self):
+ super().__init__()
+ service = activity.getSystemService(Context.SENSOR_SERVICE)
+ self.SensorManager = cast('android.hardware.SensorManager', service)
+
+ self.sensor = self.SensorManager.getDefaultSensor(
+ Sensor.TYPE_AMBIENT_TEMPERATURE)
+ self.value = None
+
+ def enable(self):
+ self.SensorManager.registerListener(
+ self, self.sensor,
+ SensorManager.SENSOR_DELAY_NORMAL
+ )
+
+ def disable(self):
+ self.SensorManager.unregisterListener(self, self.sensor)
+
+ @java_method('(Landroid/hardware/SensorEvent;)V')
+ def onSensorChanged(self, event):
+ self.value = event.values[0]
+
+ @java_method('(Landroid/hardware/Sensor;I)V')
+ def onAccuracyChanged(self, sensor, accuracy):
+ pass
+
+
+class AndroidTemperature(Temperature):
+
+ listener = None
+
+ def _get_temperature(self):
+ if self.listener and self.listener.value:
+ temperature = self.listener.value
+ return temperature
+
+ def _enable(self):
+ if not self.listener:
+ self.listener = TemperatureSensorListener()
+ self.listener.enable()
+
+ def _disable(self):
+ if self.listener:
+ self.listener.disable()
+ delattr(self, 'listener')
+
+
+def instance():
+ return AndroidTemperature()
diff --git a/sbapp/plyer/platforms/android/tts.py b/sbapp/plyer/platforms/android/tts.py
new file mode 100644
index 00000000..191dbbc7
--- /dev/null
+++ b/sbapp/plyer/platforms/android/tts.py
@@ -0,0 +1,34 @@
+from time import sleep
+from jnius import autoclass
+from plyer.facades import TTS
+from plyer.platforms.android import activity
+
+Locale = autoclass('java.util.Locale')
+TextToSpeech = autoclass('android.speech.tts.TextToSpeech')
+
+
+class AndroidTextToSpeech(TTS):
+ def _speak(self, **kwargs):
+ tts = TextToSpeech(activity, None)
+
+ tts.setLanguage(Locale.US)
+
+ retries = 0 # First try rarely succeeds due to some timing issue
+ message = kwargs.get('message')
+
+ # first try for while loop
+ speak_status = tts.speak(
+ message, TextToSpeech.QUEUE_FLUSH, None
+ )
+
+ # -1 indicates error. Let's wait and then try again
+ while retries < 100 and speak_status == -1:
+ sleep(0.1)
+ retries += 1
+ speak_status = tts.speak(
+ message, TextToSpeech.QUEUE_FLUSH, None
+ )
+
+
+def instance():
+ return AndroidTextToSpeech()
diff --git a/sbapp/plyer/platforms/android/uniqueid.py b/sbapp/plyer/platforms/android/uniqueid.py
new file mode 100644
index 00000000..fecd63f2
--- /dev/null
+++ b/sbapp/plyer/platforms/android/uniqueid.py
@@ -0,0 +1,28 @@
+'''
+Module of Android API for plyer.uniqueid.
+'''
+
+from jnius import autoclass
+from plyer.platforms.android import activity
+from plyer.facades import UniqueID
+
+Secure = autoclass('android.provider.Settings$Secure')
+
+
+class AndroidUniqueID(UniqueID):
+ '''
+ Implementation of Android uniqueid API.
+ '''
+
+ def _get_uid(self):
+ return Secure.getString(
+ activity.getContentResolver(),
+ Secure.ANDROID_ID
+ )
+
+
+def instance():
+ '''
+ Instance for facade proxy.
+ '''
+ return AndroidUniqueID()
diff --git a/sbapp/plyer/platforms/android/vibrator.py b/sbapp/plyer/platforms/android/vibrator.py
new file mode 100644
index 00000000..a318c3c4
--- /dev/null
+++ b/sbapp/plyer/platforms/android/vibrator.py
@@ -0,0 +1,63 @@
+"""Implementation Vibrator for Android."""
+
+from jnius import autoclass, cast
+from plyer.facades import Vibrator
+from plyer.platforms.android import activity
+from plyer.platforms.android import SDK_INT
+
+Context = autoclass("android.content.Context")
+vibrator_service = activity.getSystemService(Context.VIBRATOR_SERVICE)
+vibrator = cast("android.os.Vibrator", vibrator_service)
+if SDK_INT >= 26:
+ VibrationEffect = autoclass("android.os.VibrationEffect")
+
+
+class AndroidVibrator(Vibrator):
+ """Android Vibrator class.
+
+ Supported features:
+ * vibrate for some period of time.
+ * vibrate from given pattern.
+ * cancel vibration.
+ * check whether Vibrator exists.
+ """
+
+ def _vibrate(self, time=None, **kwargs):
+ if vibrator:
+ if SDK_INT >= 26:
+ vibrator.vibrate(
+ VibrationEffect.createOneShot(
+ int(1000 * time), VibrationEffect.DEFAULT_AMPLITUDE
+ )
+ )
+ else:
+ vibrator.vibrate(int(1000 * time))
+
+ def _pattern(self, pattern=None, repeat=None, **kwargs):
+ pattern = [int(1000 * time) for time in pattern]
+
+ if vibrator:
+ if SDK_INT >= 26:
+ vibrator.vibrate(
+ VibrationEffect.createWaveform(pattern, repeat)
+ )
+ else:
+ vibrator.vibrate(pattern, repeat)
+
+ def _exists(self, **kwargs):
+ if SDK_INT >= 11:
+ return vibrator.hasVibrator()
+ elif vibrator_service is None:
+ raise NotImplementedError()
+ return True
+
+ def _cancel(self, **kwargs):
+ vibrator.cancel()
+
+
+def instance():
+ """Returns Vibrator with android features.
+
+ :return: instance of class AndroidVibrator
+ """
+ return AndroidVibrator()
diff --git a/sbapp/plyer/platforms/ios/__init__.py b/sbapp/plyer/platforms/ios/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/sbapp/plyer/platforms/ios/accelerometer.py b/sbapp/plyer/platforms/ios/accelerometer.py
new file mode 100644
index 00000000..72503843
--- /dev/null
+++ b/sbapp/plyer/platforms/ios/accelerometer.py
@@ -0,0 +1,34 @@
+'''
+iOS accelerometer
+-----------------
+
+Taken from: http://pyobjus.readthedocs.org/en/latest/pyobjus_ios.html \
+ #accessing-accelerometer
+'''
+
+from plyer.facades import Accelerometer
+from pyobjus import autoclass
+
+
+class IosAccelerometer(Accelerometer):
+
+ def __init__(self):
+ super().__init__()
+ self.bridge = autoclass('bridge').alloc().init()
+ self.bridge.motionManager.setAccelerometerUpdateInterval_(0.1)
+
+ def _enable(self):
+ self.bridge.startAccelerometer()
+
+ def _disable(self):
+ self.bridge.stopAccelerometer()
+
+ def _get_acceleration(self):
+ return (
+ self.bridge.ac_x,
+ self.bridge.ac_y,
+ self.bridge.ac_z)
+
+
+def instance():
+ return IosAccelerometer()
diff --git a/sbapp/plyer/platforms/ios/barometer.py b/sbapp/plyer/platforms/ios/barometer.py
new file mode 100644
index 00000000..a5c5dc34
--- /dev/null
+++ b/sbapp/plyer/platforms/ios/barometer.py
@@ -0,0 +1,31 @@
+'''
+iOS Barometer
+-------------
+'''
+
+from plyer.facades import Barometer
+from pyobjus import autoclass
+
+
+class iOSBarometer(Barometer):
+
+ def __init__(self):
+ super().__init__()
+ self.bridge = autoclass('bridge').alloc().init()
+
+ def _enable(self):
+ self.bridge.startRelativeAltitude()
+
+ def _disable(self):
+ self.bridge.stopRelativeAltitude()
+
+ def _get_pressure(self):
+ '''
+ 1 kPa = 10 hPa
+ '''
+ return (
+ self.bridge.pressure * 10)
+
+
+def instance():
+ return iOSBarometer()
diff --git a/sbapp/plyer/platforms/ios/battery.py b/sbapp/plyer/platforms/ios/battery.py
new file mode 100644
index 00000000..818f0be4
--- /dev/null
+++ b/sbapp/plyer/platforms/ios/battery.py
@@ -0,0 +1,47 @@
+'''
+Module of iOS API for plyer.battery.
+'''
+
+from pyobjus import autoclass
+from pyobjus.dylib_manager import load_framework
+from plyer.facades import Battery
+
+load_framework('/System/Library/Frameworks/UIKit.framework')
+UIDevice = autoclass('UIDevice')
+
+
+class IOSBattery(Battery):
+ '''
+ Implementation of iOS battery API.
+ '''
+
+ def __init__(self):
+ super().__init__()
+ self.device = UIDevice.currentDevice()
+
+ def _get_state(self):
+ status = {"isCharging": None, "percentage": None}
+
+ if not self.device.batteryMonitoringEnabled:
+ self.device.setBatteryMonitoringEnabled_(True)
+
+ if self.device.batteryState == 0:
+ is_charging = None
+ elif self.device.batteryState == 2:
+ is_charging = True
+ else:
+ is_charging = False
+
+ percentage = self.device.batteryLevel * 100.
+
+ status['isCharging'] = is_charging
+ status['percentage'] = percentage
+
+ return status
+
+
+def instance():
+ '''
+ Instance for facade proxy.
+ '''
+ return IOSBattery()
diff --git a/sbapp/plyer/platforms/ios/brightness.py b/sbapp/plyer/platforms/ios/brightness.py
new file mode 100644
index 00000000..065869a4
--- /dev/null
+++ b/sbapp/plyer/platforms/ios/brightness.py
@@ -0,0 +1,27 @@
+'''
+iOS Brightness
+--------------
+'''
+
+from pyobjus import autoclass
+from plyer.facades import Brightness
+from pyobjus.dylib_manager import load_framework
+
+load_framework('/System/Library/Frameworks/UIKit.framework')
+UIScreen = autoclass('UIScreen')
+
+
+class iOSBrightness(Brightness):
+
+ def __init__(self):
+ self.screen = UIScreen.mainScreen()
+
+ def _current_level(self):
+ return self.screen.brightness * 100
+
+ def set_level(self, level):
+ self.screen.brightness = level / 100
+
+
+def instance():
+ return iOSBrightness()
diff --git a/sbapp/plyer/platforms/ios/call.py b/sbapp/plyer/platforms/ios/call.py
new file mode 100644
index 00000000..c7513993
--- /dev/null
+++ b/sbapp/plyer/platforms/ios/call.py
@@ -0,0 +1,29 @@
+'''
+IOS Call
+----------
+'''
+
+from plyer.facades import Call
+from pyobjus import autoclass, objc_str
+
+NSURL = autoclass('NSURL')
+NSString = autoclass('NSString')
+UIApplication = autoclass('UIApplication')
+
+
+class IOSCall(Call):
+
+ def _makecall(self, **kwargs):
+ tel = kwargs.get('tel')
+ url = "tel://" + tel
+ nsurl = NSURL.alloc().initWithString_(objc_str(url))
+
+ UIApplication.sharedApplication().openURL_(nsurl)
+
+ def _dialcall(self, **kwargs):
+ pass
+ # Not possible, Access not provided by iPhone SDK
+
+
+def instance():
+ return IOSCall()
diff --git a/sbapp/plyer/platforms/ios/camera.py b/sbapp/plyer/platforms/ios/camera.py
new file mode 100644
index 00000000..dfe5265b
--- /dev/null
+++ b/sbapp/plyer/platforms/ios/camera.py
@@ -0,0 +1,52 @@
+from os import remove
+from plyer.facades import Camera
+
+from plyer.utils import reify
+
+
+class iOSCamera(Camera):
+
+ @reify
+ def photos(self):
+ # pyPhotoLibrary is a ios recipe/module that
+ # interacts with the gallery and the camera on ios.
+ from photolibrary import PhotosLibrary
+ return PhotosLibrary()
+
+ def _take_picture(self, on_complete, filename=None):
+ assert(on_complete is not None)
+ self.on_complete = on_complete
+ self.filename = filename
+ photos = self.photos
+
+ if not photos.isCameraAvailable():
+ # no camera hardware
+ return False
+
+ photos.bind(on_image_captured=self.capture_callback)
+ self._capture_filename = filename
+ photos.capture_image(filename)
+ return True
+
+ def capture_callback(self, photolibrary):
+ # Image was chosen
+
+ # unbind
+ self.photos.unbind(on_image_captured=self.capture_callback)
+
+ if self.on_complete(self.filename):
+ self._remove(self.filename)
+
+ def _take_video(self, on_complete, filename=None):
+ assert(on_complete is not None)
+ raise NotImplementedError
+
+ def _remove(self, fn):
+ try:
+ remove(fn)
+ except OSError:
+ print('Could not remove photo!')
+
+
+def instance():
+ return iOSCamera()
diff --git a/sbapp/plyer/platforms/ios/compass.py b/sbapp/plyer/platforms/ios/compass.py
new file mode 100644
index 00000000..855484b3
--- /dev/null
+++ b/sbapp/plyer/platforms/ios/compass.py
@@ -0,0 +1,43 @@
+'''
+iOS Compass
+-----------
+'''
+
+from plyer.facades import Compass
+from pyobjus import autoclass
+
+
+class IosCompass(Compass):
+
+ def __init__(self):
+ super().__init__()
+ self.bridge = autoclass('bridge').alloc().init()
+ self.bridge.motionManager.setMagnetometerUpdateInterval_(0.1)
+ self.bridge.motionManager.setDeviceMotionUpdateInterval_(0.1)
+
+ def _enable(self):
+ self.bridge.startMagnetometer()
+ self.bridge.startDeviceMotionWithReferenceFrame()
+
+ def _disable(self):
+ self.bridge.stopMagnetometer()
+ self.bridge.stopDeviceMotion()
+
+ def _get_orientation(self):
+ return (
+ self.bridge.mf_x,
+ self.bridge.mf_y,
+ self.bridge.mf_z)
+
+ def _get_field_uncalib(self):
+ return (
+ self.bridge.mg_x,
+ self.bridge.mg_y,
+ self.bridge.mg_z,
+ self.bridge.mg_x - self.bridge.mf_x,
+ self.bridge.mg_y - self.bridge.mf_y,
+ self.bridge.mg_z - self.bridge.mf_z)
+
+
+def instance():
+ return IosCompass()
diff --git a/sbapp/plyer/platforms/ios/email.py b/sbapp/plyer/platforms/ios/email.py
new file mode 100644
index 00000000..e1cc7cbc
--- /dev/null
+++ b/sbapp/plyer/platforms/ios/email.py
@@ -0,0 +1,52 @@
+'''
+Module of iOS API for plyer.email.
+'''
+
+try:
+ from urllib.parse import quote
+except ImportError:
+ from urllib import quote
+
+from plyer.facades import Email
+from pyobjus import autoclass, objc_str
+from pyobjus.dylib_manager import load_framework
+
+load_framework('/System/Library/Frameworks/UIKit.framework')
+
+NSURL = autoclass('NSURL')
+NSString = autoclass('NSString')
+UIApplication = autoclass('UIApplication')
+
+
+class IOSEmail(Email):
+ '''
+ Implementation of iOS battery API.
+ '''
+
+ def _send(self, **kwargs):
+ recipient = kwargs.get('recipient')
+ subject = kwargs.get('subject')
+ text = kwargs.get('text')
+
+ uri = "mailto:"
+ if recipient:
+ uri += str(recipient)
+ if subject:
+ uri += "?" if "?" not in uri else "&"
+ uri += "subject="
+ uri += quote(str(subject))
+ if text:
+ uri += "?" if "?" not in uri else "&"
+ uri += "body="
+ uri += quote(str(text))
+
+ nsurl = NSURL.alloc().initWithString_(objc_str(uri))
+
+ UIApplication.sharedApplication().openURL_(nsurl)
+
+
+def instance():
+ '''
+ Instance for facade proxy.
+ '''
+ return IOSEmail()
diff --git a/sbapp/plyer/platforms/ios/filechooser.py b/sbapp/plyer/platforms/ios/filechooser.py
new file mode 100644
index 00000000..ba49ba96
--- /dev/null
+++ b/sbapp/plyer/platforms/ios/filechooser.py
@@ -0,0 +1,81 @@
+'''
+IOS file chooser
+--------------------
+
+This module houses the iOS implementation of the plyer FileChooser.
+
+.. versionadded:: 1.4.4
+'''
+
+from plyer.facades import FileChooser
+from pyobjus import autoclass, protocol
+from pyobjus.dylib_manager import load_framework
+
+
+load_framework('/System/Library/Frameworks/Photos.framework')
+
+
+class IOSFileChooser(FileChooser):
+ '''
+ FileChooser implementation for IOS using
+ the built-in file browser via UIImagePickerController.
+
+ .. versionadded:: 1.4.0
+ '''
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._on_selection = None
+
+ def _file_selection_dialog(self, *args, **kwargs):
+ """
+ Function called when action is required, A "mode" parameter specifies
+ which and is one of "open", "save" or "dir".
+ """
+ self._on_selection = kwargs["on_selection"]
+ if kwargs["mode"] == "open":
+ self._open()
+ else:
+ raise NotImplementedError()
+
+ def _get_picker(self):
+ """
+ Return an instantiated and configured UIImagePickerController.
+ """
+ picker = autoclass("UIImagePickerController")
+ po = picker.alloc().init()
+ po.sourceType = 0
+ po.delegate = self
+ return po
+
+ def _open(self):
+ """
+ Launch the native iOS file browser. Upon selection, the
+ `imagePickerController_didFinishPickingMediaWithInfo_` delegate is
+ called where we close the file browser and handle the result.
+ """
+ picker = self._get_picker()
+ UIApplication = autoclass('UIApplication')
+ vc = UIApplication.sharedApplication().keyWindow.rootViewController()
+ vc.presentViewController_animated_completion_(picker, True, None)
+
+ @protocol('UIImagePickerControllerDelegate')
+ def imagePickerController_didFinishPickingMediaWithInfo_(
+ self, image_picker, frozen_dict):
+ """
+ Delegate which handles the result of the image selection process.
+ """
+ image_picker.dismissViewControllerAnimated_completion_(True, None)
+
+ # Note: We need to call this Objective C class as there is currently
+ # no way to call a non-class function via pyobjus. And here,
+ # we have to use the `UIImagePNGRepresentation` to get the png
+ # representation. For this, please ensure you are using an
+ # appropriate version of kivy-ios.
+ native_image_picker = autoclass("NativeImagePicker").alloc().init()
+ path = native_image_picker.writeToPNG_(frozen_dict)
+ self._on_selection([path.UTF8String()])
+
+
+def instance():
+ return IOSFileChooser()
diff --git a/sbapp/plyer/platforms/ios/flash.py b/sbapp/plyer/platforms/ios/flash.py
new file mode 100644
index 00000000..e4bedac0
--- /dev/null
+++ b/sbapp/plyer/platforms/ios/flash.py
@@ -0,0 +1,50 @@
+# coding=utf-8
+"""
+Flash
+-----
+"""
+from plyer.facades import Flash
+from pyobjus import autoclass
+
+NSString = autoclass("NSString")
+AVCaptureDevice = autoclass("AVCaptureDevice")
+AVMediaTypeVideo = NSString.alloc().initWithUTF8String_("vide")
+AVCaptureTorchModeOff = 0
+AVCaptureTorchModeOn = 1
+
+
+class IosFlash(Flash):
+ _camera = None
+
+ def _on(self):
+ if self._camera is None:
+ self._camera_open()
+ if not self._camera:
+ return
+ self._camera.lockForConfiguration_(None)
+ try:
+ self._camera.setTorchMode(AVCaptureTorchModeOn)
+ finally:
+ self._camera.unlockForConfiguration()
+
+ def _off(self):
+ if not self._camera:
+ return
+ self._camera.lockForConfiguration_(None)
+ try:
+ self._camera.setTorchMode(AVCaptureTorchModeOff)
+ finally:
+ self._camera.unlockForConfiguration()
+
+ def _release(self):
+ pass
+
+ def _camera_open(self):
+ device = AVCaptureDevice.defaultDeviceWithMediaType_(AVMediaTypeVideo)
+ if not device:
+ return
+ self._camera = device
+
+
+def instance():
+ return IosFlash()
diff --git a/sbapp/plyer/platforms/ios/gps.py b/sbapp/plyer/platforms/ios/gps.py
new file mode 100644
index 00000000..4c52738d
--- /dev/null
+++ b/sbapp/plyer/platforms/ios/gps.py
@@ -0,0 +1,80 @@
+'''
+iOS GPS
+-----------
+'''
+
+from pyobjus import autoclass, protocol
+from pyobjus.dylib_manager import load_framework
+from plyer.facades import GPS
+
+load_framework('/System/Library/Frameworks/CoreLocation.framework')
+CLLocationManager = autoclass('CLLocationManager')
+
+
+class IosGPS(GPS):
+ def _configure(self):
+ if not hasattr(self, '_location_manager'):
+ self._location_manager = CLLocationManager.alloc().init()
+
+ def _start(self, **kwargs):
+ self._location_manager.delegate = self
+
+ self._location_manager.requestWhenInUseAuthorization()
+ # NSLocationWhenInUseUsageDescription key must exist in Info.plist
+ # file. When the authorization prompt is displayed your app goes
+ # into pause mode and if your app doesn't support background mode
+ # it will crash.
+ self._location_manager.startUpdatingLocation()
+
+ def _stop(self):
+ self._location_manager.stopUpdatingLocation()
+
+ @protocol('CLLocationManagerDelegate')
+ def locationManager_didChangeAuthorizationStatus_(self, manager, status):
+ if self.on_status:
+ s_status = ''
+ provider_status = ''
+ provider = 'standard-ios-provider'
+ if status == 0:
+ provider_status = 'provider-disabled'
+ s_status = 'notDetermined'
+ elif status == 1:
+ provider_status = 'provider-enabled'
+ s_status = 'restricted'
+ elif status == 2:
+ provider_status = 'provider-disabled'
+ s_status = 'denied'
+ elif status == 3:
+ provider_status = 'provider-enabled'
+ s_status = 'authorizedAlways'
+ elif status == 4:
+ provider_status = 'provider-enabled'
+ s_status = 'authorizedWhenInUse'
+ self.on_status(provider_status, '{}: {}'.format(
+ provider, s_status))
+
+ @protocol('CLLocationManagerDelegate')
+ def locationManager_didUpdateLocations_(self, manager, locations):
+ location = manager.location
+
+ description = location.description.UTF8String()
+ split_description = description.split('<')[-1].split('>')[0].split(',')
+
+ lat, lon = [float(coord) for coord in split_description]
+ acc = float(description.split(' +/- ')[-1].split('m ')[0])
+
+ speed = location.speed
+ altitude = location.altitude
+ course = location.course
+
+ self.on_location(
+ lat=lat,
+ lon=lon,
+ speed=speed,
+ bearing=course,
+ altitude=altitude,
+ accuracy=acc)
+
+
+def instance():
+ return IosGPS()
diff --git a/sbapp/plyer/platforms/ios/gravity.py b/sbapp/plyer/platforms/ios/gravity.py
new file mode 100644
index 00000000..a2b1be0c
--- /dev/null
+++ b/sbapp/plyer/platforms/ios/gravity.py
@@ -0,0 +1,31 @@
+'''
+iOS Gravity
+-----------
+
+'''
+
+from plyer.facades import Gravity
+from pyobjus import autoclass
+
+
+class iOSGravity(Gravity):
+
+ def __init__(self):
+ self.bridge = autoclass('bridge').alloc().init()
+ self.bridge.motionManager.setDeviceMotionUpdateInterval_(0.1)
+
+ def _enable(self):
+ self.bridge.startDeviceMotion()
+
+ def _disable(self):
+ self.bridge.stopDeviceMotion()
+
+ def _get_gravity(self):
+ return (
+ self.bridge.g_x,
+ self.bridge.g_y,
+ self.bridge.g_z)
+
+
+def instance():
+ return iOSGravity()
diff --git a/sbapp/plyer/platforms/ios/gyroscope.py b/sbapp/plyer/platforms/ios/gyroscope.py
new file mode 100644
index 00000000..340653c8
--- /dev/null
+++ b/sbapp/plyer/platforms/ios/gyroscope.py
@@ -0,0 +1,55 @@
+'''
+iOS Gyroscope
+---------------------
+'''
+
+from plyer.facades import Gyroscope
+from pyobjus import autoclass
+
+from pyobjus.dylib_manager import load_framework
+
+load_framework('/System/Library/Frameworks/UIKit.framework')
+UIDevice = autoclass('UIDevice')
+
+device = UIDevice.currentDevice()
+
+
+class IosGyroscope(Gyroscope):
+
+ def __init__(self):
+ super().__init__()
+ self.bridge = autoclass('bridge').alloc().init()
+
+ if int(device.systemVersion.UTF8String().split('.')[0]) <= 4:
+ self.bridge.motionManager.setGyroscopeUpdateInterval_(0.1)
+ else:
+ self.bridge.motionManager.setGyroUpdateInterval_(0.1)
+
+ self.bridge.motionManager.setDeviceMotionUpdateInterval_(0.1)
+
+ def _enable(self):
+ self.bridge.startGyroscope()
+ self.bridge.startDeviceMotion()
+
+ def _disable(self):
+ self.bridge.stopGyroscope()
+ self.bridge.stopDeviceMotion()
+
+ def _get_orientation(self):
+ return (
+ self.bridge.rotation_rate_x,
+ self.bridge.rotation_rate_y,
+ self.bridge.rotation_rate_z)
+
+ def _get_rotation_uncalib(self):
+ return (
+ self.bridge.gy_x,
+ self.bridge.gy_y,
+ self.bridge.gy_z,
+ self.bridge.gy_x - self.bridge.rotation_rate_x,
+ self.bridge.gy_y - self.bridge.rotation_rate_y,
+ self.bridge.gy_z - self.bridge.rotation_rate_z)
+
+
+def instance():
+ return IosGyroscope()
diff --git a/sbapp/plyer/platforms/ios/keystore.py b/sbapp/plyer/platforms/ios/keystore.py
new file mode 100644
index 00000000..289a5c01
--- /dev/null
+++ b/sbapp/plyer/platforms/ios/keystore.py
@@ -0,0 +1,23 @@
+from plyer.facades import Keystore
+from pyobjus import autoclass, objc_str
+
+NSUserDefaults = autoclass('NSUserDefaults')
+
+
+class IosKeystore(Keystore):
+
+ def _set_key(self, servicename, key, value, **kwargs):
+ NSUserDefaults.standardUserDefaults().setObject_forKey_(
+ objc_str(value), objc_str(key))
+
+ def _get_key(self, servicename, key, **kwargs):
+ ret = NSUserDefaults.standardUserDefaults().stringForKey_(
+ objc_str(key))
+ if ret is not None:
+ return ret.UTF8String()
+ else:
+ return ret
+
+
+def instance():
+ return IosKeystore()
diff --git a/sbapp/plyer/platforms/ios/sms.py b/sbapp/plyer/platforms/ios/sms.py
new file mode 100644
index 00000000..ef3acaa7
--- /dev/null
+++ b/sbapp/plyer/platforms/ios/sms.py
@@ -0,0 +1,43 @@
+'''
+IOS Sms
+----------
+'''
+
+from plyer.facades import Sms
+from pyobjus import autoclass, objc_str
+from pyobjus.dylib_manager import load_framework
+
+NSURL = autoclass('NSURL')
+NSString = autoclass('NSString')
+UIApplication = autoclass('UIApplication')
+load_framework('/System/Library/Frameworks/MessageUI.framework')
+
+
+class IOSSms(Sms):
+
+ def _send(self, **kwargs):
+ '''
+ This method provides sending messages to recipients.
+
+ Expects 2 parameters in kwargs:
+ - recipient: String type
+ - message: String type
+
+ Opens a message interface with recipient and message information.
+ '''
+ recipient = kwargs.get('recipient')
+ message = kwargs.get('message')
+ url = "sms:"
+ if recipient:
+ # Apple has not supported multiple recipients yet.
+ url += str(recipient)
+ if message:
+ # Apple has to supported it yet.
+ pass
+
+ nsurl = NSURL.alloc().initWithString_(objc_str(url))
+ UIApplication.sharedApplication().openURL_(nsurl)
+
+
+def instance():
+ return IOSSms()
diff --git a/sbapp/plyer/platforms/ios/spatialorientation.py b/sbapp/plyer/platforms/ios/spatialorientation.py
new file mode 100644
index 00000000..d42d0fa0
--- /dev/null
+++ b/sbapp/plyer/platforms/ios/spatialorientation.py
@@ -0,0 +1,31 @@
+'''
+iOS Spatial Orientation
+-----------------------
+
+'''
+
+from plyer.facades import SpatialOrientation
+from pyobjus import autoclass
+
+
+class iOSSpatialOrientation(SpatialOrientation):
+
+ def __init__(self):
+ self.bridge = autoclass('bridge').alloc().init()
+ self.bridge.motionManager.setDeviceMotionUpdateInterval_(0.1)
+
+ def _enable_listener(self):
+ self.bridge.startDeviceMotion()
+
+ def _disable_listener(self):
+ self.bridge.stopDeviceMotion()
+
+ def _get_orientation(self):
+ return (
+ self.bridge.sp_yaw,
+ self.bridge.sp_pitch,
+ self.bridge.sp_roll)
+
+
+def instance():
+ return iOSSpatialOrientation()
diff --git a/sbapp/plyer/platforms/ios/storagepath.py b/sbapp/plyer/platforms/ios/storagepath.py
new file mode 100644
index 00000000..cd8bbc39
--- /dev/null
+++ b/sbapp/plyer/platforms/ios/storagepath.py
@@ -0,0 +1,62 @@
+'''
+iOS Storage Path
+--------------------
+'''
+
+from plyer.facades import StoragePath
+from pyobjus import autoclass
+import os
+
+NSFileManager = autoclass('NSFileManager')
+
+# Directory constants (NSSearchPathDirectory enumeration)
+NSApplicationDirectory = 1
+NSDocumentDirectory = 9
+NSDownloadsDirectory = 15
+NSMoviesDirectory = 17
+NSMusicDirectory = 18
+NSPicturesDirectory = 19
+
+
+class iOSStoragePath(StoragePath):
+
+ def __init__(self):
+ self.defaultManager = NSFileManager.defaultManager()
+
+ def _get_home_dir(self):
+ return os.path.expanduser('~/')
+
+ def _get_external_storage_dir(self):
+ return 'This feature is not implemented for this platform.'
+
+ def _get_root_dir(self):
+ return 'This feature is not implemented for this platform.'
+
+ def _get_documents_dir(self):
+ return self.defaultManager.URLsForDirectory_inDomains_(
+ NSDocumentDirectory, 1).firstObject().absoluteString.UTF8String()
+
+ def _get_downloads_dir(self):
+ return self.defaultManager.URLsForDirectory_inDomains_(
+ NSDownloadsDirectory, 1).firstObject().absoluteString.UTF8String()
+
+ def _get_videos_dir(self):
+ return self.defaultManager.URLsForDirectory_inDomains_(
+ NSMoviesDirectory, 1).firstObject().absoluteString.UTF8String()
+
+ def _get_music_dir(self):
+ return self.defaultManager.URLsForDirectory_inDomains_(
+ NSMusicDirectory, 1).firstObject().absoluteString.UTF8String()
+
+ def _get_pictures_dir(self):
+ return self.defaultManager.URLsForDirectory_inDomains_(
+ NSPicturesDirectory, 1).firstObject().absoluteString.UTF8String()
+
+ def _get_application_dir(self):
+ return self.defaultManager.URLsForDirectory_inDomains_(
+ NSApplicationDirectory, 1).firstObject().absoluteString.\
+ UTF8String()
+
+
+def instance():
+ return iOSStoragePath()
diff --git a/sbapp/plyer/platforms/ios/tts.py b/sbapp/plyer/platforms/ios/tts.py
new file mode 100644
index 00000000..046769f6
--- /dev/null
+++ b/sbapp/plyer/platforms/ios/tts.py
@@ -0,0 +1,37 @@
+from pyobjus import autoclass, objc_str
+from pyobjus.dylib_manager import load_framework
+
+from plyer.facades import TTS
+
+load_framework('/System/Library/Frameworks/AVFoundation.framework')
+AVSpeechUtterance = autoclass('AVSpeechUtterance')
+AVSpeechSynthesizer = autoclass('AVSpeechSynthesizer')
+AVSpeechSynthesisVoice = autoclass('AVSpeechSynthesisVoice')
+
+
+class iOSTextToSpeech(TTS):
+ def __init__(self):
+ super().__init__()
+ self.synth = AVSpeechSynthesizer.alloc().init()
+ self.voice = None
+
+ def _set_locale(self, locale="en-US"):
+ self.voice = AVSpeechSynthesisVoice.voiceWithLanguage_(
+ objc_str(locale)
+ )
+
+ def _speak(self, **kwargs):
+ message = kwargs.get('message')
+
+ if(not self.voice):
+ self._set_locale()
+
+ utterance = \
+ AVSpeechUtterance.speechUtteranceWithString_(objc_str(message))
+
+ utterance.voice = self.voice
+ self.synth.speakUtterance_(utterance)
+
+
+def instance():
+ return iOSTextToSpeech()
diff --git a/sbapp/plyer/platforms/ios/uniqueid.py b/sbapp/plyer/platforms/ios/uniqueid.py
new file mode 100644
index 00000000..4d302499
--- /dev/null
+++ b/sbapp/plyer/platforms/ios/uniqueid.py
@@ -0,0 +1,27 @@
+'''
+Module of iOS API for plyer.uniqueid.
+'''
+
+from pyobjus import autoclass
+from pyobjus.dylib_manager import load_framework
+from plyer.facades import UniqueID
+
+load_framework('/System/Library/Frameworks/UIKit.framework')
+UIDevice = autoclass('UIDevice')
+
+
+class IOSUniqueID(UniqueID):
+ '''
+ Implementation of iOS uniqueid API.
+ '''
+
+ def _get_uid(self):
+ uuid = UIDevice.currentDevice().identifierForVendor.UUIDString()
+ return uuid.UTF8String()
+
+
+def instance():
+ '''
+ Instance for facade proxy.
+ '''
+ return IOSUniqueID()
diff --git a/sbapp/plyer/platforms/ios/vibrator.py b/sbapp/plyer/platforms/ios/vibrator.py
new file mode 100644
index 00000000..922182cd
--- /dev/null
+++ b/sbapp/plyer/platforms/ios/vibrator.py
@@ -0,0 +1,43 @@
+'''Implementation Vibrator for iOS.
+
+Install: Add AudioToolbox framework to your application.
+'''
+
+import ctypes
+from plyer.facades import Vibrator
+
+
+class IosVibrator(Vibrator):
+ '''iOS Vibrator class.
+
+ iOS doesn't support any feature.
+ All time, pattern, repetition are ignored.
+ '''
+
+ def __init__(self):
+ super().__init__()
+ try:
+ self._func = ctypes.CDLL(None).AudioServicesPlaySystemSound
+ except AttributeError:
+ self._func = None
+
+ def _vibrate(self, time=None, **kwargs):
+ # kSystemSoundID_Vibrate is 0x00000FFF
+ self._func(0xFFF)
+
+ def _pattern(self, pattern=None, repeat=None, **kwargs):
+ self._vibrate()
+
+ def _exists(self, **kwargs):
+ return self._func is not None
+
+ def _cancel(self, **kwargs):
+ pass
+
+
+def instance():
+ '''Returns Vibrator
+
+ :return: instance of class IosVibrator
+ '''
+ return IosVibrator()
diff --git a/sbapp/plyer/platforms/linux/__init__.py b/sbapp/plyer/platforms/linux/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/sbapp/plyer/platforms/linux/accelerometer.py b/sbapp/plyer/platforms/linux/accelerometer.py
new file mode 100644
index 00000000..7af283b5
--- /dev/null
+++ b/sbapp/plyer/platforms/linux/accelerometer.py
@@ -0,0 +1,35 @@
+'''
+Linux accelerometer
+---------------------
+'''
+
+from plyer.facades import Accelerometer
+import glob
+import re
+
+
+class LinuxAccelerometer(Accelerometer):
+
+ def _enable(self):
+ pass
+
+ def _disable(self):
+ pass
+
+ def _get_acceleration(self):
+ try:
+ pos = glob.glob("/sys/devices/platform/*/position")[0]
+ except IndexError:
+ raise Exception('Could not enable accelerometer!')
+
+ with open(pos, "r") as p:
+ t = p.read()
+ coords = re.findall(r"[-]?\d+\.?\d*", t)
+ # Apparently the acceleration on sysfs goes from -1000 to 1000.
+ # I divide it by 100 to make it equivalent to Android.
+ # The negative is because the coordinates are inverted on Linux
+ return [float(i) / -100 for i in coords]
+
+
+def instance():
+ return LinuxAccelerometer()
diff --git a/sbapp/plyer/platforms/linux/battery.py b/sbapp/plyer/platforms/linux/battery.py
new file mode 100644
index 00000000..fa965927
--- /dev/null
+++ b/sbapp/plyer/platforms/linux/battery.py
@@ -0,0 +1,102 @@
+'''
+Module of Linux API for plyer.battery.
+'''
+
+from math import floor
+from os import environ
+from os.path import exists, join
+from subprocess import Popen, PIPE
+from plyer.facades import Battery
+from plyer.utils import whereis_exe
+
+
+class LinuxBattery(Battery):
+ '''
+ Implementation of Linux battery API via accessing the sysclass power_supply
+ path from the kernel.
+ '''
+
+ def _get_state(self):
+ status = {"isCharging": None, "percentage": None}
+
+ kernel_bat_path = join('/sys', 'class', 'power_supply', 'BAT0')
+ uevent = join(kernel_bat_path, 'uevent')
+
+ with open(uevent) as fle:
+ lines = [
+ line.decode('utf-8').strip()
+ for line in fle.readlines()
+ ]
+ output = {
+ line.split('=')[0]: line.split('=')[1]
+ for line in lines
+ }
+
+ is_charging = output['POWER_SUPPLY_STATUS'] == 'Charging'
+ total = float(output['POWER_SUPPLY_CHARGE_FULL'])
+ now = float(output['POWER_SUPPLY_CHARGE_NOW'])
+
+ capacity = floor(now / total * 100)
+
+ status['percentage'] = capacity
+ status['isCharging'] = is_charging
+ return status
+
+
+class UPowerBattery(Battery):
+ '''
+ Implementation of UPower battery API.
+ '''
+
+ def _get_state(self):
+ # if no LANG specified, return empty string
+ old_lang = environ.get('LANG', '')
+ environ['LANG'] = 'C'
+ status = {"isCharging": None, "percentage": None}
+
+ # We are supporting only one battery now
+ # this will fail if there is no object with such path,
+ # however it's safer than 'upower -d' which provides
+ # multiple unrelated 'state' and 'percentage' keywords
+ dev = "/org/freedesktop/UPower/devices/battery_BAT0"
+ upower_process = Popen(
+ ["upower", "--show-info", dev],
+ stdout=PIPE
+ )
+ output = upower_process.communicate()[0].decode()
+ environ['LANG'] = old_lang
+ if not output:
+ return status
+ state = percentage = None
+
+ for line in output.splitlines():
+ if 'state' in line:
+ state = line.rpartition(':')[-1].strip()
+
+ if 'percentage' in line:
+ percentage = line.rpartition(':')[-1].strip()[:-1]
+
+ # switching decimal comma to dot
+ # (different LC_NUMERIC locale)
+ percentage = float(
+ percentage.replace(',', '.')
+ )
+
+ if state:
+ status['isCharging'] = state == "charging"
+ status['percentage'] = percentage
+ return status
+
+
+def instance():
+ '''
+ Instance for facade proxy.
+ '''
+ import sys
+ if whereis_exe('upower'):
+ return UPowerBattery()
+ sys.stderr.write("upower not found.")
+
+ if exists(join('/sys', 'class', 'power_supply', 'BAT0')):
+ return LinuxBattery()
+ return Battery()
diff --git a/sbapp/plyer/platforms/linux/brightness.py b/sbapp/plyer/platforms/linux/brightness.py
new file mode 100755
index 00000000..0de16985
--- /dev/null
+++ b/sbapp/plyer/platforms/linux/brightness.py
@@ -0,0 +1,29 @@
+'''
+Linux Brightness
+----------------
+
+'''
+
+from plyer.facades import Brightness
+import subprocess
+import os
+
+
+class LinuxBrightness(Brightness):
+
+ def __init__(self):
+ if os.system("which xbacklight"):
+ msg = ("It looks like 'xbacklight' is not installed. Try "
+ "installing it with your distribution's package manager.")
+ raise Exception(msg)
+
+ def _current_level(self):
+ cr_level = subprocess.check_output(["xbacklight", "-get"])
+ return str(cr_level)
+
+ def _set_level(self, level):
+ subprocess.call(["xbacklight", "-set", str(level)])
+
+
+def instance():
+ return LinuxBrightness()
diff --git a/sbapp/plyer/platforms/linux/cpu.py b/sbapp/plyer/platforms/linux/cpu.py
new file mode 100644
index 00000000..049a2d4a
--- /dev/null
+++ b/sbapp/plyer/platforms/linux/cpu.py
@@ -0,0 +1,116 @@
+'''
+Module of Linux API for plyer.cpu.
+'''
+
+from os.path import join
+from os import environ, listdir
+from subprocess import Popen, PIPE
+from plyer.facades import CPU
+from plyer.utils import whereis_exe
+
+
+class LinuxCPU(CPU):
+ '''
+ Implementation of Linux CPU API.
+ '''
+
+ def _sockets(self):
+ # physical CPU sockets (or slots) on motherboard
+ sockets = [] # list of CPU ids from kernel
+
+ # open Linux kernel data file for CPU
+ with open('/proc/cpuinfo', 'rb') as fle:
+ lines = fle.readlines()
+
+ # go through the lines and obtain physical CPU ids
+ for line in lines:
+ line = line.decode('utf-8')
+ if 'physical id' not in line:
+ continue
+ cpuid = line.split(':')[1].strip()
+ sockets.append(cpuid)
+
+ # total sockets is the length of unique CPU ids from kernel
+ sockets = len(set(sockets))
+ return sockets
+
+ def _physical(self):
+ # cores
+ physical = [] # list of CPU ids from kernel
+
+ # open Linux kernel data file for CPU
+ with open('/proc/cpuinfo', 'rb') as fle:
+ lines = fle.readlines()
+
+ # go through the lines and obtain CPU core ids
+ for line in lines:
+ line = line.decode('utf-8')
+ if 'core id' not in line:
+ continue
+ cpuid = line.split(':')[1].strip()
+ physical.append(cpuid)
+
+ # total cores (socket * core per socket)
+ # is the length of unique CPU core ids from kernel
+ physical = len(set(physical))
+ return physical
+
+ def _logical(self):
+ # cores * threads
+ logical = None
+ old_lang = environ.get('LANG', '')
+ environ['LANG'] = 'C'
+
+ _logical = Popen(['nproc', '--all'], stdout=PIPE)
+ output = _logical.communicate()[0].decode('utf-8').strip()
+ if output:
+ logical = int(output)
+
+ environ['LANG'] = old_lang
+ return logical
+
+ def _cache(self):
+ values = {key: 0 for key in ('L1', 'L2', 'L3')}
+ cpu_path = join('/sys', 'devices', 'system', 'cpu')
+
+ # get present cores from kernel device
+ with open(join(cpu_path, 'present')) as fle:
+ present = fle.read().decode('utf-8')
+ present = present.strip().split('-')
+
+ if len(present) == 2:
+ present = range(int(present[1]) + 1)
+ else:
+ present = [present[0]]
+
+ cores = ['cpu{}'.format(i) for i in present]
+ for core in cores:
+ indicies = [
+ # get 'indexN' files from 'cache' folder assuming
+ # the filename is in range index0 to index99
+ # in case a wild 'index_whatevercontent' file appears
+ fle for fle in listdir(join(cpu_path, core, 'cache'))
+ if fle.startswith('index') and len(fle) <= len('index') + 2
+ ]
+
+ for index in indicies:
+ index_type = join(cpu_path, core, 'cache', index, 'level')
+ with open(index_type, 'rb') as fle:
+ cache_level = fle.read().decode('utf-8').strip()
+ values['L{}'.format(cache_level)] += 1
+ return values
+
+ @staticmethod
+ def _numa():
+ return
+
+
+def instance():
+ '''
+ Instance for facade proxy.
+ '''
+ import sys
+ if whereis_exe('nproc'):
+ return LinuxCPU()
+ sys.stderr.write("nproc not found.")
+ return CPU()
diff --git a/sbapp/plyer/platforms/linux/devicename.py b/sbapp/plyer/platforms/linux/devicename.py
new file mode 100644
index 00000000..89413791
--- /dev/null
+++ b/sbapp/plyer/platforms/linux/devicename.py
@@ -0,0 +1,23 @@
+'''
+Module of Linux API for plyer.devicename.
+'''
+
+import socket
+from plyer.facades import DeviceName
+
+
+class LinuxDeviceName(DeviceName):
+ '''
+ Implementation of Linux DeviceName API.
+ '''
+
+ def _get_device_name(self):
+ hostname = socket.gethostname()
+ return hostname
+
+
+def instance():
+ '''
+ Instance for facade proxy.
+ '''
+ return LinuxDeviceName()
diff --git a/sbapp/plyer/platforms/linux/email.py b/sbapp/plyer/platforms/linux/email.py
new file mode 100644
index 00000000..64cbb6d5
--- /dev/null
+++ b/sbapp/plyer/platforms/linux/email.py
@@ -0,0 +1,47 @@
+'''
+Module of Linux API for plyer.email.
+'''
+
+import subprocess
+try:
+ from urllib.parse import quote
+except ImportError:
+ from urllib import quote
+from plyer.facades import Email
+from plyer.utils import whereis_exe
+
+
+class LinuxEmail(Email):
+ '''
+ Implementation of Linux email API.
+ '''
+
+ def _send(self, **kwargs):
+ recipient = kwargs.get('recipient')
+ subject = kwargs.get('subject')
+ text = kwargs.get('text')
+
+ uri = "mailto:"
+ if recipient:
+ uri += str(recipient)
+ if subject:
+ uri += "?" if "?" not in uri else "&"
+ uri += "subject="
+ uri += quote(str(subject))
+ if text:
+ uri += "?" if "?" not in uri else "&"
+ uri += "body="
+ uri += quote(str(text))
+
+ subprocess.Popen(["xdg-open", uri])
+
+
+def instance():
+ '''
+ Instance for facade proxy.
+ '''
+ import sys
+ if whereis_exe('xdg-open'):
+ return LinuxEmail()
+ sys.stderr.write("xdg-open not found.")
+ return Email()
diff --git a/sbapp/plyer/platforms/linux/filechooser.py b/sbapp/plyer/platforms/linux/filechooser.py
new file mode 100644
index 00000000..cfdb1ae9
--- /dev/null
+++ b/sbapp/plyer/platforms/linux/filechooser.py
@@ -0,0 +1,269 @@
+'''
+Linux file chooser
+------------------
+'''
+
+from plyer.facades import FileChooser
+from distutils.spawn import find_executable as which
+import os
+import subprocess as sp
+import time
+
+
+class SubprocessFileChooser:
+ '''A file chooser implementation that allows using
+ subprocess back-ends.
+ Normally you only need to override _gen_cmdline, executable,
+ separator and successretcode.
+ '''
+
+ executable = ""
+ '''The name of the executable of the back-end.
+ '''
+
+ separator = "|"
+ '''The separator used by the back-end. Override this for automatic
+ splitting, or override _split_output.
+ '''
+
+ successretcode = 0
+ '''The return code which is returned when the user doesn't close the
+ dialog without choosing anything, or when the app doesn't crash.
+ '''
+
+ path = None
+ multiple = False
+ filters = []
+ preview = False
+ title = None
+ icon = None
+ show_hidden = False
+
+ def __init__(self, *args, **kwargs):
+ self._handle_selection = kwargs.pop(
+ 'on_selection', self._handle_selection
+ )
+
+ # Simulate Kivy's behavior
+ for i in kwargs:
+ setattr(self, i, kwargs[i])
+
+ @staticmethod
+ def _handle_selection(selection):
+ '''
+ Dummy placeholder for returning selection from chooser.
+ '''
+ return selection
+
+ _process = None
+
+ def _run_command(self, cmd):
+ self._process = sp.Popen(cmd, stdout=sp.PIPE)
+ while True:
+ ret = self._process.poll()
+ if ret is not None:
+ if ret == self.successretcode:
+ out = self._process.communicate()[0].strip().decode('utf8')
+ return self._set_and_return_selection(
+ self._split_output(out))
+ else:
+ return self._set_and_return_selection(None)
+ time.sleep(0.1)
+
+ def _set_and_return_selection(self, value):
+ self.selection = value
+ self._handle_selection(value)
+ return value
+
+ def _split_output(self, out):
+ '''This methods receives the output of the back-end and turns
+ it into a list of paths.
+ '''
+ return out.split(self.separator)
+
+ def _gen_cmdline(self):
+ '''Returns the command line of the back-end, based on the current
+ properties. You need to override this.
+ '''
+ raise NotImplementedError()
+
+ def run(self):
+ return self._run_command(self._gen_cmdline())
+
+
+class ZenityFileChooser(SubprocessFileChooser):
+ '''A FileChooser implementation using Zenity (on GNU/Linux).
+
+ Not implemented features:
+ * show_hidden
+ * preview
+ '''
+
+ executable = "zenity"
+ separator = "|"
+ successretcode = 0
+
+ def _gen_cmdline(self):
+ cmdline = [
+ which(self.executable),
+ "--file-selection",
+ "--confirm-overwrite"
+ ]
+ if self.multiple:
+ cmdline += ["--multiple"]
+ if self.mode == "save":
+ cmdline += ["--save"]
+ elif self.mode == "dir":
+ cmdline += ["--directory"]
+ if self.path:
+ cmdline += ["--filename", self.path]
+ if self.title:
+ cmdline += ["--name", self.title]
+ if self.icon:
+ cmdline += ["--window-icon", self.icon]
+ for f in self.filters:
+ if type(f) == str:
+ cmdline += ["--file-filter", f]
+ else:
+ cmdline += [
+ "--file-filter",
+ "{name} | {flt}".format(name=f[0], flt=" ".join(f[1:]))
+ ]
+ return cmdline
+
+
+class KDialogFileChooser(SubprocessFileChooser):
+ '''A FileChooser implementation using KDialog (on GNU/Linux).
+
+ Not implemented features:
+ * show_hidden
+ * preview
+ '''
+
+ executable = "kdialog"
+ separator = "\n"
+ successretcode = 0
+
+ def _gen_cmdline(self):
+ cmdline = [which(self.executable)]
+
+ filt = []
+
+ for f in self.filters:
+ if type(f) == str:
+ filt += [f]
+ else:
+ filt += list(f[1:])
+
+ if self.mode == "dir":
+ cmdline += [
+ "--getexistingdirectory",
+ (self.path if self.path else os.path.expanduser("~"))
+ ]
+ elif self.mode == "save":
+ cmdline += [
+ "--getsavefilename",
+ (self.path if self.path else os.path.expanduser("~")),
+ " ".join(filt)
+ ]
+ else:
+ cmdline += [
+ "--getopenfilename",
+ (self.path if self.path else os.path.expanduser("~")),
+ " ".join(filt)
+ ]
+ if self.multiple:
+ cmdline += ["--multiple", "--separate-output"]
+ if self.title:
+ cmdline += ["--title", self.title]
+ if self.icon:
+ cmdline += ["--icon", self.icon]
+ return cmdline
+
+
+class YADFileChooser(SubprocessFileChooser):
+ '''A NativeFileChooser implementation using YAD (on GNU/Linux).
+
+ Not implemented features:
+ * show_hidden
+ '''
+
+ executable = "yad"
+ separator = "|?|"
+ successretcode = 0
+
+ def _gen_cmdline(self):
+ cmdline = [
+ which(self.executable),
+ "--file-selection",
+ "--confirm-overwrite",
+ "--geometry",
+ "800x600+150+150"
+ ]
+ if self.multiple:
+ cmdline += ["--multiple", "--separator", self.separator]
+ if self.mode == "save":
+ cmdline += ["--save"]
+ elif self.mode == "dir":
+ cmdline += ["--directory"]
+ if self.preview:
+ cmdline += ["--add-preview"]
+ if self.path:
+ cmdline += ["--filename", self.path]
+ if self.title:
+ cmdline += ["--name", self.title]
+ if self.icon:
+ cmdline += ["--window-icon", self.icon]
+ for f in self.filters:
+ if type(f) == str:
+ cmdline += ["--file-filter", f]
+ else:
+ cmdline += [
+ "--file-filter",
+ "{name} | {flt}".format(name=f[0], flt=" ".join(f[1:]))
+ ]
+ return cmdline
+
+
+CHOOSERS = {
+ "gnome": ZenityFileChooser,
+ "kde": KDialogFileChooser,
+ "yad": YADFileChooser
+}
+
+
+class LinuxFileChooser(FileChooser):
+ '''FileChooser implementation for GNu/Linux. Accepts one additional
+ keyword argument, *desktop_override*, which, if set, overrides the
+ back-end that will be used. Set it to "gnome" for Zenity, to "kde"
+ for KDialog and to "yad" for YAD (Yet Another Dialog).
+ If set to None or not set, a default one will be picked based on
+ the running desktop environment and installed back-ends.
+ '''
+
+ desktop = None
+ if (str(os.environ.get("XDG_CURRENT_DESKTOP")).lower() == "kde"
+ and which("kdialog")):
+ desktop = "kde"
+ elif (str(os.environ.get("DESKTOP_SESSION")).lower() == "trinity"
+ and which('kdialog')):
+ desktop = "kde"
+ elif which("yad"):
+ desktop = "yad"
+ elif which("zenity"):
+ desktop = "gnome"
+
+ def _file_selection_dialog(self, desktop_override=desktop, **kwargs):
+ if not desktop_override:
+ desktop_override = self.desktop
+ # This means we couldn't find any back-end
+ if not desktop_override:
+ raise OSError("No back-end available. Please install one.")
+
+ chooser = CHOOSERS[desktop_override]
+ c = chooser(**kwargs)
+ return c.run()
+
+
+def instance():
+ return LinuxFileChooser()
diff --git a/sbapp/plyer/platforms/linux/keystore.py b/sbapp/plyer/platforms/linux/keystore.py
new file mode 100644
index 00000000..105ebd31
--- /dev/null
+++ b/sbapp/plyer/platforms/linux/keystore.py
@@ -0,0 +1,19 @@
+try:
+ import keyring
+except ImportError:
+ raise NotImplementedError()
+
+from plyer.facades import Keystore
+
+
+class LinuxKeystore(Keystore):
+
+ def _set_key(self, servicename, key, value, **kwargs):
+ keyring.set_password(servicename, key, value)
+
+ def _get_key(self, servicename, key, **kwargs):
+ return keyring.get_password(servicename, key)
+
+
+def instance():
+ return LinuxKeystore()
diff --git a/sbapp/plyer/platforms/linux/notification.py b/sbapp/plyer/platforms/linux/notification.py
new file mode 100644
index 00000000..95a6472a
--- /dev/null
+++ b/sbapp/plyer/platforms/linux/notification.py
@@ -0,0 +1,108 @@
+'''
+Module of Linux API for plyer.notification.
+'''
+
+import warnings
+import subprocess
+from plyer.facades import Notification
+from plyer.utils import whereis_exe
+import os
+
+
+class NotifyDesktopPortals(Notification):
+ '''
+ Implementation of xdg-desktop-portals API.
+ '''
+
+ def _notify(self, **kwargs):
+ title = kwargs.get("title", "title")
+ body = kwargs.get("message", "body")
+
+ subprocess.run([
+ "gdbus", "call", "--session", "--dest",
+ "org.freedesktop.portal.Desktop",
+ "--object-path", "/org/freedesktop/portal/desktop", "--method",
+ "org.freedesktop.portal.Notification.AddNotification", "",
+ "{'title': <'" + title + "'>, 'body': <'" + body + "'>}"
+ ], stdout=subprocess.DEVNULL)
+
+
+class NotifySendNotification(Notification):
+ '''
+ Implementation of Linux notification API
+ using notify-send binary.
+ '''
+ def _notify(self, **kwargs):
+ icon = kwargs.get('icon', '')
+ title = kwargs.get('title', 'title')
+ hint = kwargs.get('hint', 'string::')
+ message = kwargs.get('message', 'body')
+ category = kwargs.get('category', '')
+ app_name = kwargs.get('app_name', '')
+ urgency = kwargs.get('urgency', 'normal')
+ expire_time = kwargs.get('expire_time', '0')
+
+ notify_send_args = (title,
+ message,
+ "-i", icon,
+ "-h", hint,
+ "-u", urgency,
+ "-c", category,
+ "-a", app_name,
+ "-t", expire_time)
+
+ subprocess.call(["notify-send", *notify_send_args])
+
+
+class NotifyDbus(Notification):
+ '''
+ Implementation of Linux notification API
+ using dbus library and dbus-python wrapper.
+ '''
+
+ def _notify(self, **kwargs):
+ summary = kwargs.get('title', "title")
+ body = kwargs.get('message', "body")
+ app_name = kwargs.get('app_name', '')
+ app_icon = kwargs.get('app_icon', '')
+ timeout = kwargs.get('timeout', 10)
+ actions = kwargs.get('actions', [])
+ hints = kwargs.get('hints', {})
+ replaces_id = kwargs.get('replaces_id', 0)
+
+ _bus_name = 'org.freedesktop.Notifications'
+ _object_path = '/org/freedesktop/Notifications'
+ _interface_name = _bus_name
+
+ import dbus
+ session_bus = dbus.SessionBus()
+ obj = session_bus.get_object(_bus_name, _object_path)
+ interface = dbus.Interface(obj, _interface_name)
+ interface.Notify(
+ app_name, replaces_id, app_icon,
+ summary, body, actions,
+ hints, timeout * 1000
+ )
+
+
+def instance():
+ '''
+ Instance for facade proxy.
+ '''
+ if os.path.isdir("/app"):
+ # Flatpak
+ return NotifyDesktopPortals()
+ try:
+ import dbus # noqa: F401
+ return NotifyDbus()
+ except ImportError:
+ msg = ("The Python dbus package is not installed.\n"
+ "Try installing it with your distribution's package manager, "
+ "it is usually called python-dbus or python3-dbus, but you "
+ "might have to try dbus-python instead, e.g. when using pip.")
+ warnings.warn(msg)
+
+ if whereis_exe('notify-send'):
+ return NotifySendNotification()
+ warnings.warn("notify-send not found.")
+ return Notification()
diff --git a/sbapp/plyer/platforms/linux/orientation.py b/sbapp/plyer/platforms/linux/orientation.py
new file mode 100644
index 00000000..e60fa42b
--- /dev/null
+++ b/sbapp/plyer/platforms/linux/orientation.py
@@ -0,0 +1,27 @@
+import subprocess as sb
+from plyer.facades import Orientation
+
+
+class LinuxOrientation(Orientation):
+
+ def _set_landscape(self, **kwargs):
+ self.rotate = 'normal'
+ self.screen = sb.check_output(
+ "xrandr -q | grep ' connected' | head -n 1 | cut -d ' ' -f1",
+ shell=True
+ )
+ self.screen = self.screen.decode('utf-8').split('\n')[0]
+ sb.call(["xrandr", "--output", self.screen, "--rotate", self.rotate])
+
+ def _set_portrait(self, **kwargs):
+ self.rotate = 'left'
+ self.screen = sb.check_output(
+ "xrandr -q | grep ' connected' | head -n 1 | cut -d ' ' -f1",
+ shell=True
+ )
+ self.screen = self.screen.decode('utf-8').split('\n')[0]
+ sb.call(["xrandr", "--output", self.screen, "--rotate", self.rotate])
+
+
+def instance():
+ return LinuxOrientation()
diff --git a/sbapp/plyer/platforms/linux/processors.py b/sbapp/plyer/platforms/linux/processors.py
new file mode 100644
index 00000000..74bb73af
--- /dev/null
+++ b/sbapp/plyer/platforms/linux/processors.py
@@ -0,0 +1,37 @@
+from subprocess import Popen, PIPE
+from plyer.facades import Processors
+from plyer.utils import whereis_exe
+
+from os import environ
+
+
+class LinuxProcessors(Processors):
+ def _get_state(self):
+ old_lang = environ.get('LANG')
+ environ['LANG'] = 'C'
+
+ status = {"Number_of_Processors": None}
+
+ dev = "--all"
+ nproc_process = Popen(
+ ["nproc", dev],
+ stdout=PIPE
+ )
+ output = nproc_process.communicate()[0]
+
+ environ['LANG'] = old_lang
+
+ if not output:
+ return status
+
+ status['Number_of_Processors'] = output.rstrip()
+
+ return status
+
+
+def instance():
+ import sys
+ if whereis_exe('nproc'):
+ return LinuxProcessors()
+ sys.stderr.write("nproc not found.")
+ return Processors()
diff --git a/sbapp/plyer/platforms/linux/screenshot.py b/sbapp/plyer/platforms/linux/screenshot.py
new file mode 100644
index 00000000..00eeb5c5
--- /dev/null
+++ b/sbapp/plyer/platforms/linux/screenshot.py
@@ -0,0 +1,29 @@
+import subprocess
+from os.path import join
+from plyer.facades import Screenshot
+from plyer.utils import whereis_exe
+from plyer.platforms.linux.storagepath import LinuxStoragePath
+
+
+class LinuxScreenshot(Screenshot):
+ def __init__(self, file_path=None):
+ default_path = join(
+ LinuxStoragePath().get_pictures_dir(),
+ 'screenshot.xwd'
+ )
+ super().__init__(file_path or default_path)
+
+ def _capture(self):
+ # call xwd and redirect bytes from stdout to file
+ with open(self.file_path, 'wb') as fle:
+ subprocess.call([
+ # quiet, full screen root window
+ 'xwd', '-silent', '-root',
+ ], stdout=fle)
+
+
+def instance():
+ if whereis_exe('xwd'):
+ return LinuxScreenshot()
+ else:
+ return Screenshot()
diff --git a/sbapp/plyer/platforms/linux/storagepath.py b/sbapp/plyer/platforms/linux/storagepath.py
new file mode 100755
index 00000000..736674a2
--- /dev/null
+++ b/sbapp/plyer/platforms/linux/storagepath.py
@@ -0,0 +1,72 @@
+'''
+Linux Storage Path
+--------------------
+'''
+
+from plyer.facades import StoragePath
+from os.path import expanduser, dirname, abspath, join, exists
+
+# Default paths for each name
+USER_DIRS = "/.config/user-dirs.dirs"
+
+PATHS = {
+ "DESKTOP": "Desktop",
+ "DOCUMENTS": "Documents",
+ "DOWNLOAD": "Downloads",
+ "MUSIC": "Music",
+ "PICTURES": "Pictures",
+ "VIDEOS": "Videos"
+}
+
+
+class LinuxStoragePath(StoragePath):
+
+ def _get_from_user_dirs(self, name):
+ home_dir = self._get_home_dir()
+ default = join(home_dir, PATHS[name])
+ user_dirs = join(home_dir, USER_DIRS)
+ if not exists(user_dirs):
+ return default
+
+ with open(user_dirs, "r") as f:
+ for line in f.readlines():
+ if line.startswith("XDG_" + name):
+ return line.split('"')[1]
+
+ return default
+
+ def _get_home_dir(self):
+ return expanduser('~')
+
+ def _get_external_storage_dir(self):
+ return "/media/" + self._get_home_dir().split("/")[-1]
+
+ def _get_root_dir(self):
+ return "/"
+
+ def _get_documents_dir(self):
+ directory = self._get_from_user_dirs("DOCUMENTS")
+ return directory.replace("$HOME", self._get_home_dir())
+
+ def _get_downloads_dir(self):
+ directory = self._get_from_user_dirs("DOWNLOAD")
+ return directory.replace("$HOME", self._get_home_dir())
+
+ def _get_videos_dir(self):
+ directory = self._get_from_user_dirs("VIDEOS")
+ return directory.replace("$HOME", self._get_home_dir())
+
+ def _get_music_dir(self):
+ directory = self._get_from_user_dirs("MUSIC")
+ return directory.replace("$HOME", self._get_home_dir())
+
+ def _get_pictures_dir(self):
+ directory = self._get_from_user_dirs("PICTURES")
+ return directory.replace("$HOME", self._get_home_dir())
+
+ def _get_application_dir(self):
+ return dirname(abspath(__name__))
+
+
+def instance():
+ return LinuxStoragePath()
diff --git a/sbapp/plyer/platforms/linux/tts.py b/sbapp/plyer/platforms/linux/tts.py
new file mode 100644
index 00000000..39327725
--- /dev/null
+++ b/sbapp/plyer/platforms/linux/tts.py
@@ -0,0 +1,25 @@
+import subprocess
+from plyer.facades import TTS
+from plyer.utils import whereis_exe
+
+
+class EspeakTextToSpeech(TTS):
+ ''' Speaks using the espeak program
+ '''
+ def _speak(self, **kwargs):
+ subprocess.call(["espeak", kwargs.get('message')])
+
+
+class FliteTextToSpeech(TTS):
+ ''' Speaks using the flite program
+ '''
+ def _speak(self, **kwargs):
+ subprocess.call(["flite", "-t", kwargs.get('message'), "play"])
+
+
+def instance():
+ if whereis_exe('espeak'):
+ return EspeakTextToSpeech()
+ elif whereis_exe('flite'):
+ return FliteTextToSpeech()
+ return TTS()
diff --git a/sbapp/plyer/platforms/linux/uniqueid.py b/sbapp/plyer/platforms/linux/uniqueid.py
new file mode 100644
index 00000000..44926b1c
--- /dev/null
+++ b/sbapp/plyer/platforms/linux/uniqueid.py
@@ -0,0 +1,47 @@
+'''
+Module of Linux API for plyer.uniqueid.
+'''
+
+from os import environ
+from subprocess import Popen, PIPE
+from plyer.facades import UniqueID
+from plyer.utils import whereis_exe
+
+
+class LinuxUniqueID(UniqueID):
+ '''
+ Implementation of Linux uniqueid API.
+ '''
+
+ def _get_uid(self):
+ old_lang = environ.get('LANG')
+ environ['LANG'] = 'C'
+ stdout = Popen(
+ ["lshw", "-quiet"],
+ stdout=PIPE, stderr=PIPE
+ ).communicate()[0].decode('utf-8')
+
+ output = u''
+ for line in stdout.splitlines():
+ if 'serial:' not in line:
+ continue
+ output = line
+ break
+
+ environ['LANG'] = old_lang or u''
+ result = None
+
+ if output:
+ result = output.split()[1]
+ return result
+
+
+def instance():
+ '''
+ Instance for facade proxy.
+ '''
+ import sys
+ if whereis_exe('lshw'):
+ return LinuxUniqueID()
+ sys.stderr.write("lshw not found.")
+ return UniqueID()
diff --git a/sbapp/plyer/platforms/linux/wifi.py b/sbapp/plyer/platforms/linux/wifi.py
new file mode 100644
index 00000000..73f09b66
--- /dev/null
+++ b/sbapp/plyer/platforms/linux/wifi.py
@@ -0,0 +1,482 @@
+'''
+.. note::
+ This facade depends on `nmcli` (Network Manager command line tool).
+ It's found in most of the popular GNU/Linux distributions. Support for other
+ backends is not provided yet.
+'''
+
+from subprocess import Popen, PIPE, call
+from plyer.facades import Wifi
+from plyer.utils import whereis_exe, deprecated
+
+try:
+ import wifi
+except ModuleNotFoundError as err:
+ raise ModuleNotFoundError(
+ "python-wifi not installed. try:" +
+ "`pip install --user wifi`.") from err
+
+
+class NMCLIWifi(Wifi):
+ '''
+ .. versionadded:: 1.4.0
+ '''
+
+ def __init__(self, *args, **kwargs):
+ '''
+ .. versionadded:: 1.4.0
+ '''
+
+ super().__init__(*args, **kwargs)
+ self.names = {}
+
+ @property
+ def interfaces(self):
+ '''
+ Get all the available interfaces for WiFi.
+
+ .. versionadded:: 1.4.0
+ Tested with nmcli 1.2.6.
+ '''
+ if not self._is_enabled():
+ self._enable()
+
+ # fetch the devices
+ proc = Popen([
+ 'nmcli', '--terse',
+ '--fields', 'DEVICE,TYPE',
+ 'device'
+ ], stdout=PIPE)
+ lines = proc.communicate()[0].decode('utf-8').splitlines()
+
+ # filter devices by type
+ interfaces = []
+ for line in lines:
+ # bad escape from nmcli's side :<
+ line = line.replace('\\:', '$$')
+ device, dtype = line.split(':')
+ if dtype != 'wifi':
+ continue
+ interfaces.append(device.replace('$$', ':'))
+
+ # return wifi interfaces
+ return interfaces
+
+ def _is_enabled(self):
+ '''
+ Return the status of WiFi device.
+
+ .. versionadded:: 1.4.0
+ Tested with nmcli 1.2.6.
+ '''
+ output = Popen(
+ ["nmcli", "radio", "wifi"],
+ stdout=PIPE
+ ).communicate()[0].decode('utf-8')
+
+ if output.split()[0] == 'enabled':
+ return True
+ return False
+
+ def _is_connected(self, interface=None):
+ '''
+ Return whether a specified interface is connected to a WiFi network.
+
+ .. versionadded:: 1.4.0
+ Tested with nmcli 1.2.6.
+ '''
+ if not self._is_enabled():
+ self._enable()
+ if not interface:
+ interface = self.interfaces[0]
+
+ # fetch all devices
+ proc = Popen([
+ 'nmcli', '--terse',
+ '--fields', 'DEVICE,TYPE,STATE',
+ 'device'
+ ], stdout=PIPE)
+ lines = proc.communicate()[0].decode('utf-8').splitlines()
+
+ # filter by wifi type and interface
+ connected = False
+ for line in lines:
+ line = line.replace('\\:', '$$')
+ device, dtype, state = line.split(':')
+ device = device.replace('$$', ':')
+ if dtype != 'wifi':
+ continue
+
+ if device != interface:
+ continue
+
+ if state == 'connected':
+ connected = True
+
+ return connected
+
+ def _start_scanning(self, interface=None):
+ '''
+ Start scanning for available Wi-Fi networks
+ for the specified interface.
+
+ .. versionadded:: 1.4.0
+ Tested with nmcli 1.2.6.
+ '''
+ if not self._is_enabled():
+ self._enable()
+ if not interface:
+ interface = self.interfaces[0]
+
+ # force rescan for fresh data
+ call(['nmcli', 'device', 'wifi', 'rescan', 'ifname', interface])
+
+ # get properties
+ fields = [
+ 'SSID', 'BSSID', 'MODE', 'CHAN', 'FREQ',
+ 'BARS', 'RATE', 'SIGNAL', 'SECURITY'
+ ]
+
+ # fetch all networks for interface
+ output = Popen([
+ 'nmcli', '--terse',
+ '--fields', ','.join(fields),
+ 'device', 'wifi', 'list', 'ifname', interface
+ ], stdout=PIPE).communicate()[0].decode('utf-8')
+
+ # parse output
+ for line in output.splitlines():
+ line = line.replace('\\:', '$$')
+ row = {
+ field: value
+ for field, value in zip(fields, line.split(':'))
+ }
+
+ row['BSSID'] = row['BSSID'].replace('$$', ':')
+ self.names[row['SSID']] = row
+
+ def _get_network_info(self, name):
+ '''
+ Get all the network information by network's name (SSID).
+
+ .. versionadded:: 1.4.0
+ Tested with nmcli 1.2.6.
+ '''
+ if not self.names:
+ self._start_scanning()
+
+ ret_list = {}
+ ret_list['ssid'] = self.names[name]['SSID']
+ ret_list['signal'] = self.names[name]['SIGNAL']
+
+ bars = len(self.names[name]['BARS'])
+ ret_list['quality'] = '{}/100'.format(bars / 5.0 * 100)
+ ret_list['frequency'] = self.names[name]['FREQ']
+ ret_list['bitrates'] = self.names[name]['RATE']
+
+ # wpa1, wpa2, wpa1 wpa2, wep, (none), perhaps something else
+ security = self.names[name]['SECURITY'].lower()
+ ret_list['encrypted'] = True
+ if 'wpa2' in security:
+ # wpa2, wpa2+wpa1
+ ret_list['encryption_type'] = 'wpa2'
+ elif 'wpa' in security:
+ ret_list['encryption_type'] = 'wpa'
+ elif 'wep' in security:
+ ret_list['encryption_type'] = 'wep'
+ elif 'none' in security:
+ ret_list['encrypted'] = False
+ ret_list['encryption_type'] = 'none'
+ else:
+ ret_list['encryption_type'] = security
+
+ ret_list['channel'] = int(self.names[name]['CHAN'])
+ ret_list['address'] = self.names[name]['BSSID']
+ ret_list['mode'] = self.names[name]['MODE']
+ return ret_list
+
+ def _get_available_wifi(self):
+ '''
+ Return the names of all found networks.
+
+ .. versionadded:: 1.4.0
+ Tested with nmcli 1.2.6.
+ '''
+ if not self.names:
+ self._start_scanning()
+ return list(self.names.keys())
+
+ def _connect(self, network, parameters, interface=None):
+ '''
+ Connect a specific interface to a WiFi network.
+
+ Expects 2 parameters:
+ - SSID of the network
+ - parameters: dict
+ - password: string or None
+
+ .. versionadded:: 1.4.0
+ Tested with nmcli 1.2.6.
+ '''
+ self._enable()
+ if not interface:
+ interface = self.interfaces[0]
+
+ password = parameters.get('password')
+ command = [
+ 'nmcli', 'device', 'wifi', 'connect', network,
+ 'ifname', interface
+ ]
+ if password:
+ command += ['password', password]
+ call(command)
+
+ def _disconnect(self, interface=None):
+ '''
+ Disconnect a specific interface from a WiFi network.
+
+ .. versionadded:: 1.4.0
+ Tested with nmcli 1.2.6.
+ '''
+ if not self._is_enabled():
+ return
+
+ if not interface:
+ interface = self.interfaces[0]
+
+ if self._nmcli_version() >= (1, 2, 6):
+ call(['nmcli', 'device', 'disconnect', interface])
+ else:
+ call(['nmcli', 'nm', 'enable', 'false'])
+
+ def _enable(self):
+ '''
+ Turn WiFi device on.
+
+ .. versionadded:: 1.4.0
+ Tested with nmcli 1.2.6.
+ '''
+ call(['nmcli', 'radio', 'wifi', 'on'])
+
+ def _disable(self):
+ '''
+ Turn WiFi device off.
+
+ .. versionadded:: 1.4.0
+ Tested with nmcli 1.2.6.
+ '''
+ call(['nmcli', 'radio', 'wifi', 'off'])
+
+ def _nmcli_version(self):
+ '''
+ Get nmcli version to prevent executing deprecated commands.
+
+ .. versionadded:: 1.4.0
+ Tested with nmcli 1.2.6.
+ '''
+ version = Popen(['nmcli', '-v'], stdout=PIPE)
+ version = version.communicate()[0].decode('utf-8')
+ while version and not version[0].isdigit():
+ version = version[1:]
+ return tuple(map(int, (version.split('.'))))
+
+
+@deprecated
+class LinuxWifi(Wifi):
+ '''
+ .. versionadded:: 1.2.5
+ '''
+
+ def __init__(self, *args, **kwargs):
+ '''
+ .. versionadded:: 1.4.0
+ '''
+
+ super().__init__(*args, **kwargs)
+ self.names = {}
+
+ @property
+ def interfaces(self):
+ '''
+ .. versionadded:: 1.4.0
+ '''
+
+ proc = Popen([
+ 'nmcli', '--terse',
+ '--fields', 'DEVICE,TYPE',
+ 'device'
+ ], stdout=PIPE)
+ lines = proc.communicate()[0].decode('utf-8').splitlines()
+
+ interfaces = []
+ for line in lines:
+ device, dtype = line.split(':')
+ if dtype != 'wifi':
+ continue
+ interfaces.append(device)
+
+ return interfaces
+
+ def _is_enabled(self):
+ '''
+ Returns `True` if wifi is enabled else `False`.
+
+ .. versionadded:: 1.2.5
+ .. versionchanged:: 1.3.2
+ nmcli output is properly decoded to unicode
+ '''
+ enbl = Popen(["nmcli", "radio", "wifi"], stdout=PIPE, stderr=PIPE)
+ if enbl.communicate()[0].split()[0].decode('utf-8') == "enabled":
+ return True
+ return False
+
+ def _is_connected(self, interface=None):
+ '''
+ .. versionadded:: 1.4.0
+ '''
+
+ if not interface:
+ interface = self.interfaces[0]
+
+ proc = Popen([
+ 'nmcli', '--terse',
+ '--fields', 'DEVICE,TYPE,STATE',
+ 'device'
+ ], stdout=PIPE)
+ lines = proc.communicate()[0].decode('utf-8').splitlines()
+
+ connected = False
+ for line in lines:
+ device, dtype, state = line.split(':')
+ if dtype != 'wifi':
+ continue
+
+ if device != interface:
+ continue
+
+ if state == 'connected':
+ connected = True
+
+ return connected
+
+ def _start_scanning(self, interface=None):
+ '''
+ Returns all the network information.
+
+ .. versionadded:: 1.2.5
+ .. versionchanged:: 1.3.0
+ scan only if wifi is enabled
+ '''
+
+ if not interface:
+ interface = self.interfaces[0]
+
+ if self._is_enabled():
+ list_ = list(wifi.Cell.all(interface))
+ for i in range(len(list_)):
+ self.names[list_[i].ssid] = list_[i]
+ else:
+ raise Exception('Wifi not enabled.')
+
+ def _get_network_info(self, name):
+ '''
+ Starts scanning for available Wi-Fi networks and returns the available,
+ devices.
+
+ .. versionadded:: 1.2.5
+ '''
+ ret_list = {}
+ ret_list['ssid'] = self.names[name].ssid
+ ret_list['signal'] = self.names[name].signal
+ ret_list['quality'] = self.names[name].quality
+ ret_list['frequency'] = self.names[name].frequency
+ ret_list['bitrates'] = self.names[name].bitrates
+ ret_list['encrypted'] = self.names[name].encrypted
+ ret_list['channel'] = self.names[name].channel
+ ret_list['address'] = self.names[name].address
+ ret_list['mode'] = self.names[name].mode
+ if not ret_list['encrypted']:
+ return ret_list
+ else:
+ ret_list['encryption_type'] = self.names[name].encryption_type
+ return ret_list
+
+ def _get_available_wifi(self):
+ '''
+ Returns the name of available networks.
+
+ .. versionadded:: 1.2.5
+ .. versionchanged:: 1.4.0
+ return a proper list of elements instead of dict_keys
+ '''
+ return list(self.names.keys())
+
+ def _connect(self, network, parameters, interface=None):
+ '''
+ Expects 2 parameters:
+ - name/ssid of the network.
+ - parameters: dict type
+ - password: string or None
+
+ .. versionadded:: 1.2.5
+ '''
+ if not interface:
+ interface = self.interfaces[0]
+
+ result = None
+ try:
+ self._enable()
+ finally:
+ password = parameters['password']
+ cell = self.names[network]
+ result = wifi.Scheme.for_cell(
+ interface, network, cell, password
+ )
+ return result
+
+ def _disconnect(self, interface=None):
+ '''
+ Disconnect all the networks managed by Network manager.
+
+ .. versionadded:: 1.2.5
+ '''
+ if not interface:
+ interface = self.interfaces[0]
+
+ if self._nmcli_version() >= (1, 2, 6):
+ call(['nmcli', 'dev', 'disconnect', interface])
+ else:
+ call(['nmcli', 'nm', 'enable', 'false'])
+
+ def _enable(self):
+ '''
+ Wifi interface power state is set to "ON".
+
+ .. versionadded:: 1.3.2
+ '''
+ return call(['nmcli', 'radio', 'wifi', 'on'])
+
+ def _disable(self):
+ '''
+ Wifi interface power state is set to "OFF".
+
+ .. versionadded:: 1.3.2
+ '''
+ return call(['nmcli', 'radio', 'wifi', 'off'])
+
+ def _nmcli_version(self):
+ '''
+ .. versionadded:: 1.3.2
+ '''
+ version = Popen(['nmcli', '-v'], stdout=PIPE)
+ version = version.communicate()[0].decode('utf-8')
+ while version and not version[0].isdigit():
+ version = version[1:]
+ return tuple(map(int, (version.split('.'))))
+
+
+def instance():
+ if whereis_exe('nmcli'):
+ return NMCLIWifi()
+
+ return LinuxWifi()
diff --git a/sbapp/plyer/platforms/macosx/__init__.py b/sbapp/plyer/platforms/macosx/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/sbapp/plyer/platforms/macosx/accelerometer.py b/sbapp/plyer/platforms/macosx/accelerometer.py
new file mode 100644
index 00000000..91e72c37
--- /dev/null
+++ b/sbapp/plyer/platforms/macosx/accelerometer.py
@@ -0,0 +1,25 @@
+'''
+MacOSX accelerometer
+---------------------
+'''
+
+from plyer.facades import Accelerometer
+from plyer.platforms.macosx.libs import osx_motion_sensor
+
+
+class OSXAccelerometer(Accelerometer):
+ def _enable(self):
+ try:
+ osx_motion_sensor.get_coord()
+ except Exception:
+ raise Exception('Could not enable motion sensor on this macbook!')
+
+ def _disable(self):
+ pass
+
+ def _get_acceleration(self):
+ return osx_motion_sensor.get_coord()
+
+
+def instance():
+ return OSXAccelerometer()
diff --git a/sbapp/plyer/platforms/macosx/audio.py b/sbapp/plyer/platforms/macosx/audio.py
new file mode 100644
index 00000000..3ab9ce12
--- /dev/null
+++ b/sbapp/plyer/platforms/macosx/audio.py
@@ -0,0 +1,90 @@
+from os.path import join
+
+from pyobjus import autoclass
+from pyobjus.dylib_manager import INCLUDE, load_framework
+
+from plyer.facades import Audio
+from plyer.platforms.macosx.storagepath import OSXStoragePath
+
+load_framework(INCLUDE.Foundation)
+load_framework(INCLUDE.AVFoundation)
+
+AVAudioPlayer = autoclass("AVAudioPlayer")
+AVAudioRecorder = autoclass("AVAudioRecorder")
+AVAudioFormat = autoclass("AVAudioFormat")
+NSString = autoclass('NSString')
+NSURL = autoclass('NSURL')
+NSError = autoclass('NSError').alloc()
+
+
+class OSXAudio(Audio):
+ def __init__(self, file_path=None):
+ default_path = join(
+ OSXStoragePath().get_music_dir(),
+ 'audio.wav'
+ )
+ super().__init__(file_path or default_path)
+
+ self._recorder = None
+ self._player = None
+ self._current_file = None
+
+ def _start(self):
+ # Conversion of Python file path string to Objective-C NSString
+ file_path_NSString = NSString.alloc()
+ file_path_NSString = file_path_NSString.initWithUTF8String_(
+ self._file_path
+ )
+
+ # Definition of Objective-C NSURL object for the output record file
+ # specified by NSString file path
+ file_NSURL = NSURL.alloc()
+ file_NSURL = file_NSURL.initWithString_(file_path_NSString)
+
+ # Internal audio file format specification
+ af = AVAudioFormat.alloc()
+ af = af.initWithCommonFormat_sampleRate_channels_interleaved_(
+ 1, 44100.0, 2, True
+ )
+
+ # Audio recorder instance initialization with specified file NSURL
+ # and audio file format
+ self._recorder = AVAudioRecorder.alloc()
+ self._recorder = self._recorder.initWithURL_format_error_(
+ file_NSURL, af, NSError
+ )
+
+ if not self._recorder:
+ raise Exception(NSError.code, NSError.domain)
+
+ self._recorder.record()
+
+ # Setting the currently recorded file as current file
+ # for using it as a parameter in audio player
+ self._current_file = file_NSURL
+
+ def _stop(self):
+ if self._recorder:
+ self._recorder.stop()
+ self._recorder = None
+
+ if self._player:
+ self._player.stop()
+ self._player = None
+
+ def _play(self):
+ # Audio player instance initialization with the file NSURL
+ # of the last recorded audio file
+ self._player = AVAudioPlayer.alloc()
+ self._player = self._player.initWithContentsOfURL_error_(
+ self._current_file, NSError
+ )
+
+ if not self._player:
+ raise Exception(NSError.code, NSError.domain)
+
+ self._player.play()
+
+
+def instance():
+ return OSXAudio()
diff --git a/sbapp/plyer/platforms/macosx/battery.py b/sbapp/plyer/platforms/macosx/battery.py
new file mode 100644
index 00000000..57965d32
--- /dev/null
+++ b/sbapp/plyer/platforms/macosx/battery.py
@@ -0,0 +1,59 @@
+'''
+Module of MacOS API for plyer.battery.
+'''
+
+from os import environ
+from subprocess import Popen, PIPE
+from plyer.facades import Battery
+from plyer.utils import whereis_exe
+
+
+class OSXBattery(Battery):
+ '''
+ Implementation of MacOS battery API.
+ '''
+
+ def _get_state(self):
+ old_lang = environ.get('LANG', '')
+ environ['LANG'] = 'C'
+
+ status = {"isCharging": None, "percentage": None}
+
+ ioreg_process = Popen(
+ ["ioreg", "-rc", "AppleSmartBattery"],
+ stdout=PIPE
+ )
+ output = ioreg_process.communicate()[0]
+
+ environ['LANG'] = old_lang
+
+ if not output:
+ return status
+
+ is_charging = max_capacity = current_capacity = None
+ for line in output.decode('utf-8').splitlines():
+ if 'IsCharging' in line:
+ is_charging = line.rpartition('=')[-1].strip()
+ if 'MaxCapacity' in line:
+ max_capacity = float(line.rpartition('=')[-1].strip())
+ if 'CurrentCapacity' in line:
+ current_capacity = float(line.rpartition('=')[-1].strip())
+
+ if is_charging:
+ status['isCharging'] = is_charging == "Yes"
+
+ if current_capacity and max_capacity:
+ status['percentage'] = 100.0 * current_capacity / max_capacity
+
+ return status
+
+
+def instance():
+ '''
+ Instance for facade proxy.
+ '''
+ import sys
+ if whereis_exe('ioreg'):
+ return OSXBattery()
+ sys.stderr.write("ioreg not found.")
+ return Battery()
diff --git a/sbapp/plyer/platforms/macosx/bluetooth.py b/sbapp/plyer/platforms/macosx/bluetooth.py
new file mode 100644
index 00000000..25758177
--- /dev/null
+++ b/sbapp/plyer/platforms/macosx/bluetooth.py
@@ -0,0 +1,54 @@
+'''
+Module of MacOS API for plyer.bluetooth.
+'''
+
+from subprocess import Popen, PIPE
+from plyer.facades import Bluetooth
+from plyer.utils import whereis_exe
+
+from os import environ
+
+
+class OSXBluetooth(Bluetooth):
+ '''
+ Implementation of MacOS bluetooth API.
+ '''
+
+ def _get_info(self):
+ old_lang = environ.get('LANG')
+ environ['LANG'] = 'C'
+
+ sys_profiler_process = Popen(
+ ["system_profiler", "SPBluetoothDataType"],
+ stdout=PIPE
+ )
+
+ stdout = sys_profiler_process.communicate()[0].decode('utf-8')
+ output = stdout.splitlines()
+
+ lines = []
+ for line in output:
+ if 'Bluetooth Power' not in line:
+ continue
+ lines.append(line)
+
+ if old_lang is None:
+ environ.pop('LANG')
+ else:
+ environ['LANG'] = old_lang
+
+ if output and len(lines) == 1:
+ return lines[0].split()[2]
+ else:
+ return None
+
+
+def instance():
+ '''
+ Instance for facade proxy.
+ '''
+ import sys
+ if whereis_exe('system_profiler'):
+ return OSXBluetooth()
+ sys.stderr.write("system_profiler not found.")
+ return Bluetooth()
diff --git a/sbapp/plyer/platforms/macosx/cpu.py b/sbapp/plyer/platforms/macosx/cpu.py
new file mode 100644
index 00000000..7b7da7fe
--- /dev/null
+++ b/sbapp/plyer/platforms/macosx/cpu.py
@@ -0,0 +1,62 @@
+'''
+Module of MacOS API for plyer.cpu.
+'''
+
+from subprocess import Popen, PIPE
+from plyer.facades import CPU
+from plyer.utils import whereis_exe
+
+
+class OSXCPU(CPU):
+ '''
+ Implementation of MacOS CPU API.
+ '''
+
+ @staticmethod
+ def _sockets():
+ return
+
+ def _physical(self):
+ # cores
+ physical = None
+
+ _physical = Popen(
+ ['sysctl', '-n', 'hw.physicalcpu_max'],
+ stdout=PIPE
+ )
+ output = _physical.communicate()[0].decode('utf-8').strip()
+ if output:
+ physical = int(output)
+ return physical
+
+ def _logical(self):
+ # cores * threads
+ logical = None
+
+ _logical = Popen(
+ ['sysctl', '-n', 'hw.logicalcpu_max'],
+ stdout=PIPE
+ )
+ output = _logical.communicate()[0].decode('utf-8').strip()
+ if output:
+ logical = int(output)
+ return logical
+
+ @staticmethod
+ def _cache():
+ return
+
+ @staticmethod
+ def _numa():
+ return
+
+
+def instance():
+ '''
+ Instance for facade proxy.
+ '''
+ import sys
+ if whereis_exe('sysctl'):
+ return OSXCPU()
+ sys.stderr.write('sysctl not found.')
+ return CPU()
diff --git a/sbapp/plyer/platforms/macosx/devicename.py b/sbapp/plyer/platforms/macosx/devicename.py
new file mode 100644
index 00000000..6652425e
--- /dev/null
+++ b/sbapp/plyer/platforms/macosx/devicename.py
@@ -0,0 +1,23 @@
+'''
+Module of MacOSX API for plyer.devicename.
+'''
+
+import socket
+from plyer.facades import DeviceName
+
+
+class OSXDeviceName(DeviceName):
+ '''
+ Implementation of MacOSX DeviceName API.
+ '''
+
+ def _get_device_name(self):
+ hostname = socket.gethostname()
+ return hostname
+
+
+def instance():
+ '''
+ Instance for facade proxy.
+ '''
+ return OSXDeviceName()
diff --git a/sbapp/plyer/platforms/macosx/email.py b/sbapp/plyer/platforms/macosx/email.py
new file mode 100644
index 00000000..49c5d4dd
--- /dev/null
+++ b/sbapp/plyer/platforms/macosx/email.py
@@ -0,0 +1,49 @@
+'''
+Module of MacOS API for plyer.email.
+'''
+
+import subprocess
+
+try:
+ from urllib.parse import quote
+except ImportError:
+ from urllib import quote
+
+from plyer.facades import Email
+from plyer.utils import whereis_exe
+
+
+class MacOSXEmail(Email):
+ '''
+ Implementation of MacOS email API.
+ '''
+
+ def _send(self, **kwargs):
+ recipient = kwargs.get('recipient')
+ subject = kwargs.get('subject')
+ text = kwargs.get('text')
+
+ uri = "mailto:"
+ if recipient:
+ uri += str(recipient)
+ if subject:
+ uri += "?" if "?" not in uri else "&"
+ uri += "subject="
+ uri += quote(str(subject))
+ if text:
+ uri += "?" if "?" not in uri else "&"
+ uri += "body="
+ uri += quote(str(text))
+
+ subprocess.Popen(["open", uri])
+
+
+def instance():
+ '''
+ Instance for facade proxy.
+ '''
+ import sys
+ if whereis_exe('open'):
+ return MacOSXEmail()
+ sys.stderr.write("open not found.")
+ return Email()
diff --git a/sbapp/plyer/platforms/macosx/filechooser.py b/sbapp/plyer/platforms/macosx/filechooser.py
new file mode 100644
index 00000000..85fb91c7
--- /dev/null
+++ b/sbapp/plyer/platforms/macosx/filechooser.py
@@ -0,0 +1,126 @@
+'''
+Mac OS X file chooser
+---------------------
+'''
+
+from plyer.facades import FileChooser
+from pyobjus import autoclass, objc_arr, objc_str
+from pyobjus.dylib_manager import load_framework, INCLUDE
+
+load_framework(INCLUDE.AppKit)
+NSURL = autoclass('NSURL')
+NSOpenPanel = autoclass('NSOpenPanel')
+NSSavePanel = autoclass('NSSavePanel')
+NSOKButton = 1
+
+
+class MacFileChooser:
+ '''A native implementation of file chooser dialogs using Apple's API
+ through pyobjus.
+
+ Not implemented features:
+ * filters (partial, wildcards are converted to extensions if possible.
+ Pass the Mac-specific "use_extensions" if you can provide
+ Mac OS X-compatible to avoid automatic conversion)
+ * multiple (only for save dialog. Available in open dialog)
+ * icon
+ * preview
+ '''
+
+ mode = "open"
+ path = None
+ multiple = False
+ filters = []
+ preview = False
+ title = None
+ icon = None
+ show_hidden = False
+ use_extensions = False
+
+ def __init__(self, *args, **kwargs):
+ self._handle_selection = kwargs.pop(
+ 'on_selection', self._handle_selection
+ )
+
+ # Simulate Kivy's behavior
+ for i in kwargs:
+ setattr(self, i, kwargs[i])
+
+ @staticmethod
+ def _handle_selection(selection):
+ '''
+ Dummy placeholder for returning selection from chooser.
+ '''
+ return selection
+
+ def run(self):
+ panel = None
+ if self.mode in ("open", "dir", "dir_and_files"):
+ panel = NSOpenPanel.openPanel()
+
+ panel.setCanChooseDirectories_(self.mode != "open")
+ panel.setCanChooseFiles_(self.mode != "dir")
+
+ if self.multiple:
+ panel.setAllowsMultipleSelection_(True)
+ elif self.mode == "save":
+ panel = NSSavePanel.savePanel()
+ else:
+ assert False, self.mode
+
+ panel.setCanCreateDirectories_(True)
+ panel.setShowsHiddenFiles_(self.show_hidden)
+
+ if self.title:
+ panel.setTitle_(objc_str(self.title))
+
+ # Mac OS X does not support wildcards unlike the other platforms.
+ # This tries to convert wildcards to "extensions" when possible,
+ # ans sets the panel to also allow other file types, just to be safe.
+ if self.filters:
+ filthies = []
+ for f in self.filters:
+ if type(f) == str:
+ f = (None, f)
+ for s in f[1:]:
+ if not self.use_extensions:
+ if s.strip().endswith("*"):
+ continue
+ pystr = s.strip().split("*")[-1].split(".")[-1]
+ filthies.append(objc_str(pystr))
+
+ ftypes_arr = objc_arr(*filthies)
+ # todo: switch to allowedContentTypes
+ panel.setAllowedFileTypes_(ftypes_arr)
+ panel.setAllowsOtherFileTypes_(not self.use_extensions)
+
+ if self.path:
+ url = NSURL.fileURLWithPath_(self.path)
+ panel.setDirectoryURL_(url)
+
+ selection = None
+
+ if panel.runModal():
+ if self.mode == "save" or not self.multiple:
+ selection = [panel.filename().UTF8String()]
+ else:
+ filename = panel.filenames()
+ selection = [
+ filename.objectAtIndex_(x).UTF8String()
+ for x in range(filename.count())]
+
+ self._handle_selection(selection)
+
+ return selection
+
+
+class MacOSXFileChooser(FileChooser):
+ '''
+ FileChooser implementation for macOS using NSOpenPanel, NSSavePanel.
+ '''
+ def _file_selection_dialog(self, **kwargs):
+ return MacFileChooser(**kwargs).run()
+
+
+def instance():
+ return MacOSXFileChooser()
diff --git a/sbapp/plyer/platforms/macosx/keystore.py b/sbapp/plyer/platforms/macosx/keystore.py
new file mode 100644
index 00000000..11e60e89
--- /dev/null
+++ b/sbapp/plyer/platforms/macosx/keystore.py
@@ -0,0 +1,19 @@
+try:
+ import keyring
+except ImportError:
+ raise NotImplementedError()
+
+from plyer.facades import Keystore
+
+
+class OSXKeystore(Keystore):
+
+ def _set_key(self, servicename, key, value, **kwargs):
+ keyring.set_password(servicename, key, value)
+
+ def _get_key(self, servicename, key, **kwargs):
+ return keyring.get_password(servicename, key)
+
+
+def instance():
+ return OSXKeystore()
diff --git a/sbapp/plyer/platforms/macosx/libs/__init__.py b/sbapp/plyer/platforms/macosx/libs/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/sbapp/plyer/platforms/macosx/libs/osx_motion_sensor.py b/sbapp/plyer/platforms/macosx/libs/osx_motion_sensor.py
new file mode 100644
index 00000000..d49df433
--- /dev/null
+++ b/sbapp/plyer/platforms/macosx/libs/osx_motion_sensor.py
@@ -0,0 +1,128 @@
+import ctypes
+from ctypes import (
+ Structure, cdll, sizeof,
+ c_int8, c_int16, c_size_t
+)
+from ctypes.util import find_library
+import platform
+
+ERROR_DICT = {
+ "0": "IOKit Framework not found, is this OSX?",
+ "-1": "No SMCMotionSensor service",
+ "-2": "No sms device",
+ "-3": "Could not open motion sensor device",
+ "-4": "Did not receive any coordinates"
+}
+
+IOKit = cdll.LoadLibrary(find_library('IOKit'))
+
+
+class data_structure(Structure):
+ _fields_ = [
+ ('x', c_int16),
+ ('y', c_int16),
+ ('z', c_int16),
+ ('pad', c_int8 * 34),
+ ]
+
+
+void_p = ctypes.POINTER(ctypes.c_int)
+
+kern_return_t = ctypes.c_int
+KERN_SUCCESS = 0
+KERN_FUNC = 5 # SMC Motion Sensor on MacBook Pro
+
+mach_port_t = void_p
+MACH_PORT_NULL = 0
+
+io_object_t = ctypes.c_int
+io_object_t = ctypes.c_int
+io_iterator_t = void_p
+io_object_t = void_p
+io_connect_t = void_p
+IOItemCount = ctypes.c_uint
+
+CFMutableDictionaryRef = void_p
+
+
+def is_os_64bit():
+ return platform.machine().endswith('64')
+
+
+def read_sms():
+ result = kern_return_t()
+ masterPort = mach_port_t()
+
+ result = IOKit.IOMasterPort(MACH_PORT_NULL, ctypes.byref(masterPort))
+
+ IOKit.IOServiceMatching.restype = CFMutableDictionaryRef
+ matchingDictionary = IOKit.IOServiceMatching("SMCMotionSensor")
+
+ iterator = io_iterator_t()
+ result = IOKit.IOServiceGetMatchingServices(
+ masterPort, matchingDictionary,
+ ctypes.byref(iterator)
+ )
+
+ if (result != KERN_SUCCESS):
+ raise ("No coordinates received!")
+ return -1, None
+
+ IOKit.IOIteratorNext.restype = io_object_t
+ smsDevice = IOKit.IOIteratorNext(iterator)
+
+ if not smsDevice:
+ return -2, None
+
+ dataPort = io_connect_t()
+ result = IOKit.IOServiceOpen(
+ smsDevice, IOKit.mach_task_self(),
+ 0, ctypes.byref(dataPort)
+ )
+
+ if (result != KERN_SUCCESS):
+ return -3, None
+
+ inStructure = data_structure()
+ outStructure = data_structure()
+
+ if(is_os_64bit() or hasattr(IOKit, 'IOConnectCallStructMethod')):
+ structureInSize = IOItemCount(sizeof(data_structure))
+ structureOutSize = c_size_t(sizeof(data_structure))
+
+ result = IOKit.IOConnectCallStructMethod(
+ dataPort, KERN_FUNC,
+ ctypes.byref(inStructure), structureInSize,
+ ctypes.byref(outStructure), ctypes.byref(structureOutSize)
+ )
+ else:
+ structureInSize = IOItemCount(sizeof(data_structure))
+ structureOutSize = IOItemCount(sizeof(data_structure))
+
+ result = IOKit.IOConnectMethodStructureIStructureO(
+ dataPort, KERN_FUNC,
+ structureInSize, ctypes.byref(structureOutSize),
+ ctypes.byref(inStructure), ctypes.byref(outStructure)
+ )
+
+ IOKit.IOServiceClose(dataPort)
+
+ if (result != KERN_SUCCESS):
+ return -4, None
+
+ return 1, outStructure
+
+
+def get_coord():
+ if not IOKit:
+ raise Exception(ERROR_DICT["0"])
+
+ ret, data = read_sms()
+
+ if (ret > 0):
+ if(data.x):
+ return (data.x, data.y, data.z)
+ else:
+ return (None, None, None)
+ else:
+ raise Exception(ERROR_DICT[str(ret)])
diff --git a/sbapp/plyer/platforms/macosx/libs/osx_paths.py b/sbapp/plyer/platforms/macosx/libs/osx_paths.py
new file mode 100644
index 00000000..9b26a784
--- /dev/null
+++ b/sbapp/plyer/platforms/macosx/libs/osx_paths.py
@@ -0,0 +1,21 @@
+import ctypes
+import os
+
+
+def NSIterateSearchPaths(directory):
+ LibraryPath = ("/System/Library/Frameworks/CoreFoundation.framework/"
+ "Versions/A/CoreFoundation")
+ CoreFound = ctypes.cdll.LoadLibrary(LibraryPath)
+ NSStartSearchPathEnumeration = CoreFound.NSStartSearchPathEnumeration
+ NSGetNextSearchPathEnumeration = CoreFound.NSGetNextSearchPathEnumeration
+ PATH_MAX = os.pathconf('/', os.pathconf_names['PC_PATH_MAX'])
+ PATH_ENCODING = 'utf8'
+ path_buffer = ctypes.create_string_buffer(PATH_MAX)
+ # paths = [] <- fixme, possible list of paths in directory
+ state = NSStartSearchPathEnumeration(directory, 1)
+ while True:
+ state = NSGetNextSearchPathEnumeration(state, path_buffer)
+ if state == 0:
+ break
+ path = os.path.expanduser(path_buffer.value.decode(PATH_ENCODING))
+ return path
diff --git a/sbapp/plyer/platforms/macosx/notification.py b/sbapp/plyer/platforms/macosx/notification.py
new file mode 100644
index 00000000..ed78ab11
--- /dev/null
+++ b/sbapp/plyer/platforms/macosx/notification.py
@@ -0,0 +1,51 @@
+'''
+Module of MacOS API for plyer.notification.
+'''
+
+from plyer.facades import Notification
+
+from pyobjus import (
+ autoclass, protocol, objc_str, ObjcBOOL
+)
+from pyobjus.dylib_manager import (
+ load_framework, INCLUDE
+)
+
+load_framework(INCLUDE.AppKit)
+load_framework(INCLUDE.Foundation)
+
+NSUserNotification = autoclass('NSUserNotification')
+NSUserNotificationCenter = autoclass('NSUserNotificationCenter')
+
+
+class OSXNotification(Notification):
+ '''
+ Implementation of MacOS notification API.
+ '''
+
+ def _notify(self, **kwargs):
+ title = kwargs.get('title', '')
+ message = kwargs.get('message', '')
+ app_name = kwargs.get('app_name', '')
+ # app_icon, timeout, ticker are not supported (yet)
+
+ notification = NSUserNotification.alloc().init()
+ notification.setTitle_(objc_str(title))
+ notification.setSubtitle_(objc_str(app_name))
+ notification.setInformativeText_(objc_str(message))
+
+ usrnotifctr = NSUserNotificationCenter.defaultUserNotificationCenter()
+ usrnotifctr.setDelegate_(self)
+ usrnotifctr.deliverNotification_(notification)
+
+ @protocol('NSUserNotificationCenterDelegate')
+ def userNotificationCenter_shouldPresentNotification_(
+ self, center, notification):
+ return ObjcBOOL(True)
+
+
+def instance():
+ '''
+ Instance for facade proxy.
+ '''
+ return OSXNotification()
diff --git a/sbapp/plyer/platforms/macosx/screenshot.py b/sbapp/plyer/platforms/macosx/screenshot.py
new file mode 100644
index 00000000..c76766ad
--- /dev/null
+++ b/sbapp/plyer/platforms/macosx/screenshot.py
@@ -0,0 +1,27 @@
+import subprocess
+from os.path import join
+from plyer.facades import Screenshot
+from plyer.utils import whereis_exe
+from plyer.platforms.macosx.storagepath import OSXStoragePath
+
+
+class OSXScreenshot(Screenshot):
+ def __init__(self, file_path=None):
+ default_path = join(
+ OSXStoragePath().get_pictures_dir().replace('file://', ''),
+ 'screenshot.png'
+ )
+ super().__init__(file_path or default_path)
+
+ def _capture(self):
+ subprocess.call([
+ 'screencapture',
+ self.file_path
+ ])
+
+
+def instance():
+ if whereis_exe('screencapture'):
+ return OSXScreenshot()
+ else:
+ return Screenshot()
diff --git a/sbapp/plyer/platforms/macosx/storagepath.py b/sbapp/plyer/platforms/macosx/storagepath.py
new file mode 100644
index 00000000..027b04ee
--- /dev/null
+++ b/sbapp/plyer/platforms/macosx/storagepath.py
@@ -0,0 +1,62 @@
+'''
+MacOS X Storage Path
+--------------------
+'''
+
+from plyer.facades import StoragePath
+from pyobjus import autoclass
+
+NSFileManager = autoclass('NSFileManager')
+
+# Directory constants (NSSearchPathDirectory enumeration)
+NSApplicationDirectory = 1
+NSDocumentDirectory = 9
+NSDownloadsDirectory = 15
+NSMoviesDirectory = 17
+NSMusicDirectory = 18
+NSPicturesDirectory = 19
+
+
+class OSXStoragePath(StoragePath):
+
+ def __init__(self):
+ self.defaultManager = NSFileManager.defaultManager()
+
+ def _get_home_dir(self):
+ home_dir_NSURL = self.defaultManager.homeDirectoryForCurrentUser
+ return home_dir_NSURL.absoluteString.UTF8String()
+
+ def _get_external_storage_dir(self):
+ return 'Method not implemented for current platform.'
+
+ def _get_root_dir(self):
+ return '/'
+
+ def _get_documents_dir(self):
+ return self.defaultManager.URLsForDirectory_inDomains_(
+ NSDocumentDirectory, 1).firstObject().absoluteString.UTF8String()
+
+ def _get_downloads_dir(self):
+ return self.defaultManager.URLsForDirectory_inDomains_(
+ NSDownloadsDirectory, 1).firstObject().absoluteString.UTF8String()
+
+ def _get_videos_dir(self):
+ return self.defaultManager.URLsForDirectory_inDomains_(
+ NSMoviesDirectory, 1).firstObject().absoluteString.UTF8String()
+
+ def _get_music_dir(self):
+ return self.defaultManager.URLsForDirectory_inDomains_(
+ NSMusicDirectory, 1).firstObject().absoluteString.UTF8String()
+
+ def _get_pictures_dir(self):
+ return self.defaultManager.URLsForDirectory_inDomains_(
+ NSPicturesDirectory, 1).firstObject().absoluteString.UTF8String()
+
+ def _get_application_dir(self):
+ return self.defaultManager.URLsForDirectory_inDomains_(
+ NSApplicationDirectory, 1
+ ).firstObject().absoluteString.UTF8String()
+
+
+def instance():
+ return OSXStoragePath()
diff --git a/sbapp/plyer/platforms/macosx/tts.py b/sbapp/plyer/platforms/macosx/tts.py
new file mode 100644
index 00000000..755e820d
--- /dev/null
+++ b/sbapp/plyer/platforms/macosx/tts.py
@@ -0,0 +1,25 @@
+import subprocess
+from plyer.facades import TTS
+from plyer.utils import whereis_exe
+
+
+class NativeSayTextToSpeech(TTS):
+ '''Speaks using the native OSX 'say' command
+ '''
+ def _speak(self, **kwargs):
+ subprocess.call(["say", kwargs.get('message')])
+
+
+class EspeakTextToSpeech(TTS):
+ '''Speaks using the espeak program
+ '''
+ def _speak(self, **kwargs):
+ subprocess.call(["espeak", kwargs.get('message')])
+
+
+def instance():
+ if whereis_exe('say'):
+ return NativeSayTextToSpeech()
+ elif whereis_exe('espeak'):
+ return EspeakTextToSpeech()
+ return TTS()
diff --git a/sbapp/plyer/platforms/macosx/uniqueid.py b/sbapp/plyer/platforms/macosx/uniqueid.py
new file mode 100644
index 00000000..dc841535
--- /dev/null
+++ b/sbapp/plyer/platforms/macosx/uniqueid.py
@@ -0,0 +1,47 @@
+'''
+Module of MacOS API for plyer.uniqueid.
+'''
+
+from os import environ
+from subprocess import Popen, PIPE
+from plyer.facades import UniqueID
+from plyer.utils import whereis_exe
+
+
+class OSXUniqueID(UniqueID):
+ '''
+ Implementation of MacOS uniqueid API.
+ '''
+
+ def _get_uid(self):
+ old_lang = environ.get('LANG')
+ environ['LANG'] = 'C'
+
+ ioreg_process = Popen(["ioreg", "-l"], stdout=PIPE)
+ grep_process = Popen(
+ ["grep", "IOPlatformSerialNumber"],
+ stdin=ioreg_process.stdout, stdout=PIPE
+ )
+ ioreg_process.stdout.close()
+ output = grep_process.communicate()[0]
+
+ if old_lang is None:
+ environ.pop('LANG')
+ else:
+ environ['LANG'] = old_lang
+
+ result = None
+ if output:
+ result = output.split()[3][1:-1]
+ return result
+
+
+def instance():
+ '''
+ Instance for facade proxy.
+ '''
+ import sys
+ if whereis_exe('ioreg'):
+ return OSXUniqueID()
+ sys.stderr.write("ioreg not found.")
+ return UniqueID()
diff --git a/sbapp/plyer/platforms/macosx/wifi.py b/sbapp/plyer/platforms/macosx/wifi.py
new file mode 100644
index 00000000..32c02ab6
--- /dev/null
+++ b/sbapp/plyer/platforms/macosx/wifi.py
@@ -0,0 +1,147 @@
+from pyobjus import autoclass
+from pyobjus.dylib_manager import load_framework, INCLUDE
+
+from plyer.facades import Wifi
+
+load_framework(INCLUDE.Foundation)
+load_framework(INCLUDE.CoreWLAN)
+
+CWInterface = autoclass('CWInterface')
+CWNetwork = autoclass('CWNetwork')
+CWWiFiClient = autoclass('CWWiFiClient')
+NSArray = autoclass('NSArray')
+NSDictionary = autoclass('NSDictionary')
+NSString = autoclass('NSString')
+
+
+class OSXWifi(Wifi):
+ names = {}
+
+ def _is_enabled(self):
+ '''
+ Returns `True` if the Wifi is enabled else returns `False`.
+ '''
+ return CWWiFiClient.sharedWiFiClient().interface().powerOn()
+
+ def _get_network_info(self, name):
+ '''
+ Returns all the network information.
+ '''
+
+ accessNetworkType = self.names[name].accessNetworkType
+ aggregateRSSI = self.names[name].aggregateRSSI
+ beaconInterval = self.names[name].beaconInterval
+ bssid = self.names[name].bssid.UTF8String()
+ countryCode = self.names[name].countryCode
+ hasInternet = self.names[name].hasInternet
+ hasInterworkingIE = self.names[name].hasInterworkingIE
+ hessid = self.names[name].hessid
+ ibss = self.names[name].ibss
+ isAdditionalStepRequiredForAccess = \
+ self.names[name].isAdditionalStepRequiredForAccess
+ isCarPlayNetwork = self.names[name].isCarPlayNetwork
+ isEmergencyServicesReachable = \
+ self.names[name].isEmergencyServicesReachable
+ isPasspoint = self.names[name].isPasspoint
+ isPersonalHotspot = self.names[name].isPersonalHotspot
+ isUnauthenticatedEmergencyServiceAccessible = \
+ self.names[name].isUnauthenticatedEmergencyServiceAccessible
+ noiseMeasurement = self.names[name].noiseMeasurement
+ physicalLayerMode = self.names[name].physicalLayerMode
+ rssiValue = self.names[name].rssiValue
+ securityType = self.names[name].securityType
+ ssid = self.names[name].ssid.UTF8String()
+ supportsEasyConnect = self.names[name].supportsEasyConnect
+ supportsWPS = self.names[name].supportsWPS
+ venueGroup = self.names[name].venueGroup
+ venueType = self.names[name].venueType
+
+ return {'accessNetworkType': accessNetworkType,
+ 'aggregateRSSI': aggregateRSSI,
+ 'beaconInterval': beaconInterval,
+ 'bssid': bssid,
+ 'countryCode': countryCode,
+ 'hasInternet': hasInternet,
+ 'hasInterworkingIE': hasInterworkingIE,
+ 'hessid': hessid,
+ 'ibss': ibss,
+ 'isAdditionalStepRequiredForAccess':
+ isAdditionalStepRequiredForAccess,
+ 'isCarPlayNetwork': isCarPlayNetwork,
+ 'isEmergencyServicesReachable': isEmergencyServicesReachable,
+ 'isPasspoint': isPasspoint,
+ 'isPersonalHotspot': isPersonalHotspot,
+ 'isUnauthenticatedEmergencyServiceAccessible':
+ isUnauthenticatedEmergencyServiceAccessible,
+ 'noiseMeasurement': noiseMeasurement,
+ 'physicalLayerMode': physicalLayerMode,
+ 'rssiValue': rssiValue,
+ 'securityType': securityType,
+ 'ssid': ssid,
+ 'supportsEasyConnect': supportsEasyConnect,
+ 'supportsWPS': supportsWPS,
+ 'venueGroup': venueGroup,
+ 'venueType': venueType}
+
+ def _start_scanning(self):
+ '''
+ Starts scanning for available Wi-Fi networks.
+ '''
+ if self._is_enabled():
+ self.names = {}
+ c = CWInterface.interface()
+ scan = c.scanForNetworksWithName_error_(None, None)
+ cnt = scan.allObjects().count()
+ for i in range(cnt):
+ self.names[
+ scan.allObjects().objectAtIndex_(i).ssid.UTF8String()
+ ] = scan.allObjects().objectAtIndex_(i)
+ else:
+ raise Exception("Wifi not enabled.")
+
+ def _get_available_wifi(self):
+ '''
+ Returns the name of available networks.
+ '''
+ return self.names.keys()
+
+ def _connect(self, network, parameters):
+ '''
+ Expects 2 parameters:
+ - name/ssid of the network.
+ - password: dict type
+ '''
+ password = parameters['password']
+ network_object = self.names[network]
+ CWInterface.interface().associateToNetwork_password_error_(
+ network_object,
+ password,
+ None)
+ return
+
+ def _disconnect(self):
+ '''
+ Disconnect from network.
+ '''
+ CWInterface.interface().disassociate()
+ return
+
+ def _disable(self):
+ '''
+ Wifi interface power state is set to "OFF".
+ '''
+
+ interface = CWWiFiClient.sharedWiFiClient().interface()
+ interface.setPower_error_(False, None)
+
+ def _enable(self):
+ '''
+ Wifi interface power state is set to "ON".
+ '''
+
+ interface = CWWiFiClient.sharedWiFiClient().interface()
+ interface.setPower_error_(True, None)
+
+
+def instance():
+ return OSXWifi()
diff --git a/sbapp/plyer/platforms/win/__init__.py b/sbapp/plyer/platforms/win/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/sbapp/plyer/platforms/win/audio.py b/sbapp/plyer/platforms/win/audio.py
new file mode 100644
index 00000000..c9d8f357
--- /dev/null
+++ b/sbapp/plyer/platforms/win/audio.py
@@ -0,0 +1,398 @@
+'''
+Documentation:
+http://docs.microsoft.com/en-us/windows/desktop/Multimedia
+
+.. versionadded:: 1.4.0
+'''
+
+from os.path import join
+
+from ctypes import windll
+from ctypes import (
+ sizeof, c_void_p, c_ulonglong, c_ulong,
+ c_wchar_p, byref, Structure, create_string_buffer
+)
+from ctypes.wintypes import DWORD, UINT
+
+from plyer.facades import Audio
+from plyer.platforms.win.storagepath import WinStoragePath
+
+# DWORD_PTR i.e. ULONG_PTR, 32/64bit
+ULONG_PTR = c_ulonglong if sizeof(c_void_p) == 8 else c_ulong
+
+# device specific symbols
+MCI_OPEN = 0x803
+MCI_OPEN_TYPE = 0x2000
+MCI_OPEN_ELEMENT = 512
+MCI_RECORD = 0x80F
+MCI_STOP = 0x808
+MCI_SAVE = 0x813
+MCI_PLAY = 0x806
+MCI_CLOSE = 0x804
+
+# recorder specific symbols
+MCI_FROM = 4
+MCI_TO = 8
+MCI_WAIT = 2
+MCI_SAVE_FILE = 256
+
+
+class MCI_OPEN_PARMS(Structure):
+ '''
+ Struct for MCI_OPEN message parameters.
+
+ .. versionadded:: 1.4.0
+ '''
+
+ _fields_ = [
+ ('mciOpenParms', ULONG_PTR),
+ ('wDeviceID', UINT),
+ ('lpstrDeviceType', c_wchar_p),
+ ('lpstrElementName', c_wchar_p),
+ ('lpstrAlias', c_wchar_p)
+ ]
+
+
+class MCI_RECORD_PARMS(Structure):
+ '''
+ Struct for MCI_RECORD message parameters.
+
+ http://docs.microsoft.com/en-us/windows/desktop/Multimedia/mci-record-parms
+
+ .. versionadded:: 1.4.0
+ '''
+
+ _fields_ = [
+ ('dwCallback', ULONG_PTR),
+ ('dwFrom', DWORD),
+ ('dwTo', DWORD)
+ ]
+
+
+class MCI_SAVE_PARMS(Structure):
+ '''
+ Struct for MCI_SAVE message parameters.
+
+ http://docs.microsoft.com/en-us/windows/desktop/Multimedia/mci-save-parms
+
+ .. versionadded:: 1.4.0
+ '''
+
+ _fields_ = [
+ ('dwCallback', ULONG_PTR),
+ ('lpfilename', c_wchar_p)
+ ]
+
+
+class MCI_PLAY_PARMS(Structure):
+ '''
+ Struct for MCI_PLAY message parameters.
+
+ http://docs.microsoft.com/en-us/windows/desktop/Multimedia/mci-play-parms
+
+ .. versionadded:: 1.4.0
+ '''
+
+ _fields_ = [
+ ('dwCallback', ULONG_PTR),
+ ('dwFrom', DWORD),
+ ('dwTo', DWORD)
+ ]
+
+
+def send_command(device, msg, flags, params):
+ '''
+ Generic mciSendCommandW() wrapper with error handler.
+ All parameters are required as for mciSendCommandW().
+ In case of no `params` passed, use `None`, that value
+ won't be dereferenced.
+
+ .. versionadded:: 1.4.0
+ '''
+
+ multimedia = windll.winmm
+ send_command_w = multimedia.mciSendCommandW
+ get_error = multimedia.mciGetErrorStringW
+
+ # error text buffer
+ # by API specification 128 is max, however the API sometimes
+ # kind of does not respect the documented bounds and returns
+ # more characters than buffer length...?!
+ error_len = 128
+
+ # big enough to prevent API accidentally segfaulting
+ error_text = create_string_buffer(error_len * 2)
+
+ # open a recording device with a new file
+ error_code = send_command_w(
+ device, # device ID
+ msg,
+ flags,
+
+ # reference to parameters structure or original value
+ # in case of params=False/0/None/...
+ byref(params) if params else params
+ )
+
+ # handle error messages if any
+ if error_code:
+ # device did not open, raise an exception
+ get_error(error_code, byref(error_text), error_len)
+ error_text = error_text.raw.replace(b'\x00', b'').decode('utf-8')
+
+ # either it can close already open device or it will fail because
+ # the device is in non-closable state, but the end result is the same
+ # and it makes no sense to parse MCI_CLOSE's error in this case
+ send_command_w(device, MCI_CLOSE, 0, None)
+ raise Exception(error_code, error_text)
+
+ # return params struct because some commands write into it
+ # to pass some values out of the local function scope
+ return params
+
+
+class WinRecorder:
+ '''
+ Generic wrapper for MCI_RECORD handling the filenames and device closing
+ in the same approach like it is used for other platforms.
+
+ .. versionadded:: 1.4.0
+ '''
+
+ def __init__(self, device, filename):
+ self._device = device
+ self._filename = filename
+
+ @property
+ def device(self):
+ '''
+ Public property returning device ID.
+
+ .. versionadded:: 1.4.0
+ '''
+ return self._device
+
+ @property
+ def filename(self):
+ '''
+ Public property returning filename for current recording.
+
+ .. versionadded:: 1.4.0
+ '''
+ return self._filename
+
+ def record(self):
+ '''
+ Start recording a WAV sound.
+
+ .. versionadded:: 1.4.0
+ '''
+ send_command(
+ device=self.device,
+ msg=MCI_RECORD,
+ flags=0,
+ params=None
+ )
+
+ def stop(self):
+ '''
+ Stop recording and save the data to a file path
+ self.filename. Wait until the file is written.
+ Close the device afterwards.
+
+ .. versionadded:: 1.4.0
+ '''
+
+ # stop the recording first
+ send_command(
+ device=self.device,
+ msg=MCI_STOP,
+ flags=MCI_WAIT,
+ params=None
+ )
+
+ # choose filename for the WAV file
+ save_params = MCI_SAVE_PARMS()
+ save_params.lpfilename = self.filename
+
+ # save the sound data to a file and wait
+ # until it ends writing to the file
+ send_command(
+ device=self.device,
+ msg=MCI_SAVE,
+ flags=MCI_SAVE_FILE | MCI_WAIT,
+ params=save_params
+ )
+
+ # close the recording device
+ send_command(
+ device=self.device,
+ msg=MCI_CLOSE,
+ flags=0,
+ params=None
+ )
+
+
+class WinPlayer:
+ '''
+ Generic wrapper for MCI_PLAY handling the device closing.
+
+ .. versionadded:: 1.4.0
+ '''
+
+ def __init__(self, device):
+ self._device = device
+
+ @property
+ def device(self):
+ '''
+ Public property returning device ID.
+
+ .. versionadded:: 1.4.0
+ '''
+ return self._device
+
+ def play(self):
+ '''
+ Start playing a WAV sound.
+
+ .. versionadded:: 1.4.0
+ '''
+ play_params = MCI_PLAY_PARMS()
+ play_params.dwFrom = 0
+
+ send_command(
+ device=self.device,
+ msg=MCI_PLAY,
+ flags=MCI_FROM,
+ params=play_params
+ )
+
+ def stop(self):
+ '''
+ Stop playing a WAV sound and close the device.
+
+ .. versionadded:: 1.4.0
+ '''
+ send_command(
+ device=self.device,
+ msg=MCI_STOP,
+ flags=MCI_WAIT,
+ params=None
+ )
+
+ # close the playing device
+ send_command(
+ device=self.device,
+ msg=MCI_CLOSE,
+ flags=0,
+ params=None
+ )
+
+
+class WinAudio(Audio):
+ '''
+ Windows implementation of audio recording and audio playing.
+
+ .. versionadded:: 1.4.0
+ '''
+
+ def __init__(self, file_path=None):
+ # default path unless specified otherwise
+ default_path = join(
+ WinStoragePath().get_music_dir(),
+ 'audio.wav'
+ )
+ super().__init__(file_path or default_path)
+
+ self._recorder = None
+ self._player = None
+ self._current_file = None
+
+ def _start(self):
+ '''
+ Start recording a WAV sound in the background asynchronously.
+
+ .. versionadded:: 1.4.0
+ '''
+
+ # clean everything before recording in case
+ # there is a different device open
+ self._stop()
+
+ # create structure and set device parameters
+ open_params = MCI_OPEN_PARMS()
+ open_params.lpstrDeviceType = 'waveaudio'
+ open_params.lpstrElementName = ''
+
+ # open a new device for recording
+ open_params = send_command(
+ device=0, # device ID before opening
+ msg=MCI_OPEN,
+
+ # empty filename in lpstrElementName
+ # device type in lpstrDeviceType
+ flags=MCI_OPEN_ELEMENT | MCI_OPEN_TYPE,
+ params=open_params
+ )
+
+ # get recorder with device id and path for saving
+ self._recorder = WinRecorder(
+ device=open_params.wDeviceID,
+ filename=self._file_path
+ )
+ self._recorder.record()
+
+ # Setting the currently recorded file as current file
+ # for using it as a parameter in audio player
+ self._current_file = self._recorder.filename
+
+ def _stop(self):
+ '''
+ Stop recording or playing of a WAV sound.
+
+ .. versionadded:: 1.4.0
+ '''
+
+ if self._recorder:
+ self._recorder.stop()
+ self._recorder = None
+
+ if self._player:
+ self._player.stop()
+ self._player = None
+
+ def _play(self):
+ '''
+ Play a WAV sound from a file. Prioritize latest recorded file before
+ default file path from WinAudio.
+
+ .. versionadded:: 1.4.0
+ '''
+
+ # create structure and set device parameters
+ open_params = MCI_OPEN_PARMS()
+ open_params.lpstrDeviceType = 'waveaudio'
+ open_params.lpstrElementName = self._current_file or self._file_path
+
+ # open a new device for playing
+ open_params = send_command(
+ device=0, # device ID before opening
+ msg=MCI_OPEN,
+
+ # existing filename in lpstrElementName
+ # device type in lpstrDeviceType
+ flags=MCI_OPEN_ELEMENT | MCI_OPEN_TYPE,
+ params=open_params
+ )
+
+ # get recorder with device id and path for saving
+ self._player = WinPlayer(device=open_params.wDeviceID)
+ self._player.play()
+
+
+def instance():
+ '''
+ Instance for facade proxy.
+ '''
+ return WinAudio()
diff --git a/sbapp/plyer/platforms/win/battery.py b/sbapp/plyer/platforms/win/battery.py
new file mode 100644
index 00000000..861d9048
--- /dev/null
+++ b/sbapp/plyer/platforms/win/battery.py
@@ -0,0 +1,36 @@
+'''
+Module of Windows API for plyer.battery.
+'''
+
+from plyer.platforms.win.libs.batterystatus import battery_status
+from plyer.facades import Battery
+from ctypes.wintypes import BYTE
+
+
+class WinBattery(Battery):
+ '''
+ Implementation of Windows battery API.
+ '''
+
+ def _get_state(self):
+ CHARGING = BYTE(8).value
+ UNKNOWN_STATUS = BYTE(255).value
+ status = {"isCharging": None, "percentage": None}
+
+ query = battery_status()
+
+ if not query:
+ return status
+
+ status["isCharging"] = (query["BatteryFlag"] != UNKNOWN_STATUS) and \
+ (query["BatteryFlag"] & CHARGING > 0)
+ status["percentage"] = query["BatteryLifePercent"]
+
+ return status
+
+
+def instance():
+ '''
+ Instance for facade proxy.
+ '''
+ return WinBattery()
diff --git a/sbapp/plyer/platforms/win/cpu.py b/sbapp/plyer/platforms/win/cpu.py
new file mode 100644
index 00000000..d1262dcb
--- /dev/null
+++ b/sbapp/plyer/platforms/win/cpu.py
@@ -0,0 +1,252 @@
+'''
+Module of Windows API for plyer.cpu.
+'''
+
+from ctypes import (
+ c_ulonglong, c_ulong, byref,
+ Structure, POINTER, Union, windll, create_string_buffer,
+ sizeof, cast, c_void_p, c_uint32
+)
+from ctypes.wintypes import (
+ BYTE, DWORD, WORD
+)
+
+from plyer.facades import CPU
+
+
+KERNEL = windll.kernel32
+ERROR_INSUFFICIENT_BUFFER = 0x0000007A
+
+
+class CacheType:
+ '''
+ Win API PROCESSOR_CACHE_TYPE enum.
+ '''
+
+ unified = 0
+ instruction = 1
+ data = 2
+ trace = 3
+
+
+class RelationshipType:
+ '''
+ Win API LOGICAL_PROCESSOR_RELATIONSHIP enum.
+ '''
+
+ processor_core = 0 # logical proc sharing single core
+ numa_node = 1 # logical proc sharing single NUMA node
+ cache = 2 # logical proc sharing cache
+ processor_package = 3 # logical proc sharing physical package
+ group = 4 # logical proc sharing processor group
+ all = 0xffff # logical proc info for all groups
+
+
+class CacheDescriptor(Structure):
+ '''
+ Win API CACHE_DESCRIPTOR struct.
+ '''
+
+ _fields_ = [
+ ('Level', BYTE),
+ ('Associativity', BYTE),
+ ('LineSize', WORD),
+ ('Size', DWORD),
+ ('Type', DWORD)
+ ]
+
+
+class ProcessorCore(Structure):
+ '''
+ Win API ProcessorCore struct.
+ '''
+
+ _fields_ = [('Flags', BYTE)]
+
+
+class NumaNode(Structure):
+ '''
+ Win API NumaNode struct.
+ '''
+
+ _fields_ = [('NodeNumber', DWORD)]
+
+
+class SystemLPIUnion(Union):
+ '''
+ Win API SYSTEM_LOGICAL_PROCESSOR_INFORMATION union without name.
+ '''
+
+ _fields_ = [
+ ('ProcessorCore', ProcessorCore),
+ ('NumaNode', NumaNode),
+ ('Cache', CacheDescriptor),
+ ('Reserved', c_ulonglong)
+ ]
+
+
+class SystemLPI(Structure):
+ '''
+ Win API SYSTEM_LOGICAL_PROCESSOR_INFORMATION struct.
+ '''
+
+ _fields_ = [
+ ('ProcessorMask', c_ulong),
+ ('Relationship', c_ulong),
+ ('LPI', SystemLPIUnion)
+ ]
+
+
+class WinCPU(CPU):
+ '''
+ Implementation of Windows CPU API.
+ '''
+
+ @staticmethod
+ def _countbits(mask):
+ # make sure the correct ULONG_PTR size is used on 64bit
+ # https://docs.microsoft.com/en-us/windows/
+ # desktop/WinProg/windows-data-types
+ # note: not a pointer per-se, != PULONG_PTR
+ ulong_ptr = c_ulonglong if sizeof(c_void_p) == 8 else c_ulong
+ # note: c_ulonglong only on 64bit, otherwise c_ulong
+
+ # DWORD == c_uint32
+ # https://docs.microsoft.com/en-us/windows/
+ # desktop/WinProg/windows-data-types
+ lshift = c_uint32(sizeof(ulong_ptr) * 8 - 1)
+ assert lshift.value in (31, 63), lshift # 32 or 64 bits - 1
+
+ lshift = lshift.value
+ test = 1 << lshift
+ assert test % 2 == 0, test
+
+ count = 0
+ i = 0
+ while i <= lshift:
+ i += 1
+
+ # do NOT remove!!!
+ # test value has to be %2 == 0,
+ # except the last case where the value is 1,
+ # so that int(test) == int(float(test))
+ # and the mask bit is counted correctly
+ assert test % 2 == 0 or float(test) == 1.0, test
+
+ # https://stackoverflow.com/a/1746642/5994041
+ # note: useful to print(str(bin(int(...)))[2:])
+ count += 1 if (mask & int(test)) else 0
+ test /= 2
+
+ return count
+
+ def _logprocinfo(self, relationship):
+ get_logical_process_info = KERNEL.GetLogicalProcessorInformation
+
+ # first call with no structure to get the real size of the required
+ buff_length = c_ulong(0)
+ result = get_logical_process_info(None, byref(buff_length))
+ assert not result, result
+ error = KERNEL.GetLastError()
+ assert error == ERROR_INSUFFICIENT_BUFFER, error
+ assert buff_length, buff_length
+
+ # create buffer from the real winapi buffer length
+ buff = create_string_buffer(buff_length.value)
+
+ # call again with buffer pointer + the same length as arguments
+ result = get_logical_process_info(buff, byref(buff_length))
+ assert result, (result, KERNEL.GetLastError())
+
+ # memory size of one LPI struct in the array of LPI structs
+ offset = sizeof(SystemLPI) # ok
+ values = {
+ key: 0 for key in (
+ 'relationship', 'mask',
+ 'L1', 'L2', 'L3'
+ )
+ }
+
+ for i in range(0, buff_length.value, offset):
+ slpi = cast(
+ buff[i: i + offset],
+ POINTER(SystemLPI)
+ ).contents
+
+ if slpi.Relationship != relationship:
+ continue
+
+ values['relationship'] += 1
+ values['mask'] += self._countbits(slpi.ProcessorMask)
+
+ if slpi.LPI.Cache.Level == 1:
+ values['L1'] += 1
+ elif slpi.LPI.Cache.Level == 2:
+ values['L2'] += 1
+ elif slpi.LPI.Cache.Level == 3:
+ values['L3'] += 1
+
+ return values
+
+ def _sockets(self):
+ # physical CPU sockets (or slots) on motherboard
+ return self._logprocinfo(
+ RelationshipType.processor_package
+ )['relationship']
+
+ def _physical(self):
+ # cores
+ return self._logprocinfo(
+ RelationshipType.processor_core
+ )['relationship']
+
+ def _logical(self):
+ # cores * threads
+ # if hyperthreaded core -> more than one logical processor
+ return self._logprocinfo(
+ RelationshipType.processor_core
+ )['mask']
+
+ def _cache(self):
+ # L1, L2, L3 cache count
+ result = self._logprocinfo(
+ RelationshipType.cache
+ )
+ return {
+ key: result[key]
+ for key in result
+ if key in ('L1', 'L2', 'L3')
+ }
+
+ def _numa(self):
+ # numa nodes
+ return self._logprocinfo(
+ RelationshipType.numa_node
+ )['relationship']
+
+
+def instance():
+ '''
+ Instance for facade proxy.
+ '''
+ return WinCPU()
+
+
+# Resources:
+# GetLogicalProcessInformation
+# https://msdn.microsoft.com/en-us/library/ms683194(v=vs.85).aspx
+
+# SYSTEM_LOGICAL_PROCESSOR_INFORMATION
+# https://msdn.microsoft.com/en-us/library/ms686694(v=vs.85).aspx
+
+# LOGICAL_PROCESSOR_RELATIONSHIP enum (0 - 4, 0xffff)
+# https://msdn.microsoft.com/2ada52f0-70ec-4146-9ef7-9af3b08996f9
+
+# CACHE_DESCRIPTOR struct
+# https://msdn.microsoft.com/38cfa605-831c-45ef-a99f-55f42b2b56e9
+
+# PROCESSOR_CACHE_TYPE
+# https://msdn.microsoft.com/23044f67-e944-43c2-8c75-3d2fba87cb3c
+
+# C example
+# https://msdn.microsoft.com/en-us/904d2d35-f419-4e8f-a689-f39ed926644c
diff --git a/sbapp/plyer/platforms/win/devicename.py b/sbapp/plyer/platforms/win/devicename.py
new file mode 100644
index 00000000..d35d76bd
--- /dev/null
+++ b/sbapp/plyer/platforms/win/devicename.py
@@ -0,0 +1,23 @@
+'''
+Module of Win API for plyer.devicename.
+'''
+
+import socket
+from plyer.facades import DeviceName
+
+
+class WinDeviceName(DeviceName):
+ '''
+ Implementation of Linux DeviceName API.
+ '''
+
+ def _get_device_name(self):
+ hostname = socket.gethostname()
+ return hostname
+
+
+def instance():
+ '''
+ Instance for facade proxy.
+ '''
+ return WinDeviceName()
diff --git a/sbapp/plyer/platforms/win/email.py b/sbapp/plyer/platforms/win/email.py
new file mode 100644
index 00000000..4c0f9b76
--- /dev/null
+++ b/sbapp/plyer/platforms/win/email.py
@@ -0,0 +1,46 @@
+'''
+Module of Windows API for plyer.email.
+'''
+
+import os
+try:
+ from urllib.parse import quote
+except ImportError:
+ from urllib import quote
+from plyer.facades import Email
+
+
+class WindowsEmail(Email):
+ '''
+ Implementation of Windows email API.
+ '''
+
+ def _send(self, **kwargs):
+ recipient = kwargs.get('recipient')
+ subject = kwargs.get('subject')
+ text = kwargs.get('text')
+
+ uri = "mailto:"
+ if recipient:
+ uri += str(recipient)
+ if subject:
+ uri += "?" if "?" not in uri else "&"
+ uri += "subject="
+ uri += quote(str(subject))
+ if text:
+ uri += "?" if "?" not in uri else "&"
+ uri += "body="
+ uri += quote(str(text))
+
+ # WE + startfile are available only on Windows
+ try:
+ os.startfile(uri)
+ except WindowsError:
+ print("Warning: unable to find a program able to send emails.")
+
+
+def instance():
+ '''
+ Instance for facade proxy.
+ '''
+ return WindowsEmail()
diff --git a/sbapp/plyer/platforms/win/filechooser.py b/sbapp/plyer/platforms/win/filechooser.py
new file mode 100644
index 00000000..d61fdc11
--- /dev/null
+++ b/sbapp/plyer/platforms/win/filechooser.py
@@ -0,0 +1,162 @@
+'''
+Windows file chooser
+--------------------
+'''
+
+from plyer.facades import FileChooser
+from win32com.shell.shell import (
+ SHBrowseForFolder as browse,
+ SHGetPathFromIDList as get_path
+)
+from win32com.shell import shellcon
+import win32gui
+import win32con
+import pywintypes
+from os.path import dirname, splitext, join, isdir
+
+
+class Win32FileChooser:
+ '''A native implementation of NativeFileChooser using the
+ Win32 API on Windows.
+
+ Not Implemented features (all dialogs):
+ * preview
+ * icon
+
+ Not implemented features (in directory selection only - it's limited
+ by Windows itself):
+ * preview
+ * window-icon
+
+ Known issues:
+ * non-existins folders such as: Network, Control Panel, My Computer, Trash,
+ Library and likes will raise a COM error. The path does not exist, nor
+ a user can open from or save to such path.
+ '''
+
+ path = None
+ multiple = False
+ filters = []
+ preview = False
+ title = None
+ icon = None
+ show_hidden = False
+
+ def __init__(self, *args, **kwargs):
+ self._handle_selection = kwargs.pop(
+ 'on_selection', self._handle_selection
+ )
+
+ # Simulate Kivy's behavior
+ for i in kwargs:
+ setattr(self, i, kwargs[i])
+
+ @staticmethod
+ def _handle_selection(selection):
+ '''
+ Dummy placeholder for returning selection from chooser.
+ '''
+ return selection
+
+ def run(self):
+ self.selection = []
+ try:
+ if self.mode != "dir":
+ args = {}
+
+ if self.path:
+ if isdir(self.path):
+ args["InitialDir"] = self.path
+ else:
+ args["InitialDir"] = dirname(self.path)
+ _, ext = splitext(self.path)
+ args["File"] = self.path
+ args["DefExt"] = ext and ext[1:] # no period
+
+ args["Title"] = self.title if self.title else "Pick a file..."
+ args["CustomFilter"] = 'Other file types\x00*.*\x00'
+ args["FilterIndex"] = 1
+ file = ""
+ if "File" in args:
+ file = args["File"]
+ args["File"] = file + ("\x00" * 4096)
+
+ # e.g. open_file(filters=['*.txt', '*.py'])
+ filters = ""
+ for f in self.filters:
+ if type(f) == str:
+ filters += (f + "\x00") * 2
+ else:
+ filters += f[0] + "\x00" + ";".join(f[1:]) + "\x00"
+ args["Filter"] = filters
+
+ flags = win32con.OFN_OVERWRITEPROMPT
+ flags |= win32con.OFN_HIDEREADONLY
+
+ if self.multiple:
+ flags |= win32con.OFN_ALLOWMULTISELECT
+ flags |= win32con.OFN_EXPLORER
+ if self.show_hidden:
+ flags |= win32con.OFN_FORCESHOWHIDDEN
+
+ args["Flags"] = flags
+
+ try:
+ if self.mode == "open":
+ self.fname, _, _ = win32gui.GetOpenFileNameW(**args)
+ elif self.mode == "save":
+ self.fname, _, _ = win32gui.GetSaveFileNameW(**args)
+ except pywintypes.error as e:
+ # if canceled, it's not really an error
+ if not e.winerror:
+ self._handle_selection(self.selection)
+ return self.selection
+ raise
+
+ if self.fname:
+ if self.multiple:
+ seq = str(self.fname).split("\x00")
+ if len(seq) > 1:
+ dir_n, base_n = seq[0], seq[1:]
+ self.selection = [
+ join(dir_n, i) for i in base_n
+ ]
+ else:
+ self.selection = seq
+ else:
+ self.selection = str(self.fname).split("\x00")
+
+ else: # dir mode
+ BIF_EDITBOX = shellcon.BIF_EDITBOX
+ BIF_NEWDIALOGSTYLE = 0x00000040
+ # From http://goo.gl/UDqCqo
+ pidl, name, images = browse(
+ win32gui.GetDesktopWindow(),
+ None,
+ self.title if self.title else "Pick a folder...",
+ BIF_NEWDIALOGSTYLE | BIF_EDITBOX, None, None
+ )
+
+ # pidl is None when nothing is selected
+ # and e.g. the dialog is closed afterwards with Cancel
+ if pidl:
+ self.selection = [str(get_path(pidl).decode('utf-8'))]
+
+ except (RuntimeError, pywintypes.error, Exception):
+ # ALWAYS! let user know what happened
+ import traceback
+ traceback.print_exc()
+ self._handle_selection(self.selection)
+ return self.selection
+
+
+class WinFileChooser(FileChooser):
+ '''FileChooser implementation for Windows, using win3all.
+ '''
+
+ def _file_selection_dialog(self, **kwargs):
+ return Win32FileChooser(**kwargs).run()
+
+
+def instance():
+ return WinFileChooser()
diff --git a/sbapp/plyer/platforms/win/keystore.py b/sbapp/plyer/platforms/win/keystore.py
new file mode 100644
index 00000000..0065a6ab
--- /dev/null
+++ b/sbapp/plyer/platforms/win/keystore.py
@@ -0,0 +1,19 @@
+try:
+ import keyring
+except Exception:
+ raise NotImplementedError()
+
+from plyer.facades import Keystore
+
+
+class WinKeystore(Keystore):
+
+ def _set_key(self, servicename, key, value, **kwargs):
+ keyring.set_password(servicename, key, value)
+
+ def _get_key(self, servicename, key, **kwargs):
+ return keyring.get_password(servicename, key)
+
+
+def instance():
+ return WinKeystore()
diff --git a/sbapp/plyer/platforms/win/libs/__init__.py b/sbapp/plyer/platforms/win/libs/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/sbapp/plyer/platforms/win/libs/balloontip.py b/sbapp/plyer/platforms/win/libs/balloontip.py
new file mode 100644
index 00000000..5494f4e5
--- /dev/null
+++ b/sbapp/plyer/platforms/win/libs/balloontip.py
@@ -0,0 +1,206 @@
+# -- coding: utf-8 --
+'''
+Module of Windows API for creating taskbar balloon tip
+notification in the taskbar's tray notification area.
+'''
+
+__all__ = ('WindowsBalloonTip', 'balloon_tip')
+
+
+import time
+import ctypes
+import atexit
+from threading import RLock
+
+from plyer.platforms.win.libs import win_api_defs
+
+
+WS_OVERLAPPED = 0x00000000
+WS_SYSMENU = 0x00080000
+WM_DESTROY = 2
+CW_USEDEFAULT = 8
+
+LR_LOADFROMFILE = 16
+LR_DEFAULTSIZE = 0x0040
+IDI_APPLICATION = 32512
+IMAGE_ICON = 1
+
+NOTIFYICON_VERSION_4 = 4
+NIM_ADD = 0
+NIM_MODIFY = 1
+NIM_DELETE = 2
+NIM_SETVERSION = 4
+NIF_MESSAGE = 1
+NIF_ICON = 2
+NIF_TIP = 4
+NIF_INFO = 0x10
+NIIF_USER = 4
+NIIF_LARGE_ICON = 0x20
+
+
+class WindowsBalloonTip:
+ '''
+ Implementation of balloon tip notifications through Windows API.
+
+ * Register Window class name:
+ https://msdn.microsoft.com/en-us/library/windows/desktop/ms632596.aspx
+ * Create an overlapped window using the registered class.
+ - It's hidden everywhere in GUI unless ShowWindow(handle, SW_SHOW)
+ function is called.
+ * Show/remove a tray icon and a balloon tip notification.
+
+ Each instance is a separate notification with different parameters.
+ Can be used with Threads.
+ '''
+
+ _class_atom = 0
+ _wnd_class_ex = None
+ _hwnd = None
+ _hicon = None
+ _balloon_icon = None
+ _notify_data = None
+ _count = 0
+ _lock = RLock()
+
+ @staticmethod
+ def _get_unique_id():
+ '''
+ Keep track of each created balloon tip notification names,
+ so that they can be easily identified even from outside.
+
+ Make sure the count is shared between all the instances
+ i.e. use a lock, so that _count class variable is incremented
+ safely when using balloon tip notifications with Threads.
+ '''
+
+ WindowsBalloonTip._lock.acquire()
+ val = WindowsBalloonTip._count
+ WindowsBalloonTip._count += 1
+ WindowsBalloonTip._lock.release()
+ return val
+
+ def __init__(self, title, message, app_name, app_icon='',
+ timeout=10, **kwargs):
+ '''
+ The app_icon parameter, if given, is an .ICO file.
+ '''
+ atexit.register(self.__del__)
+
+ wnd_class_ex = win_api_defs.get_WNDCLASSEXW()
+ class_name = 'PlyerTaskbar' + str(WindowsBalloonTip._get_unique_id())
+
+ wnd_class_ex.lpszClassName = class_name
+
+ # keep ref to it as long as window is alive
+ wnd_class_ex.lpfnWndProc = win_api_defs.WindowProc(
+ win_api_defs.DefWindowProcW
+ )
+ wnd_class_ex.hInstance = win_api_defs.GetModuleHandleW(None)
+ if wnd_class_ex.hInstance is None:
+ raise Exception('Could not get windows module instance.')
+
+ class_atom = win_api_defs.RegisterClassExW(wnd_class_ex)
+ if class_atom == 0:
+ raise Exception('Could not register the PlyerTaskbar class.')
+
+ self._class_atom = class_atom
+ self._wnd_class_ex = wnd_class_ex
+
+ # create window
+ self._hwnd = win_api_defs.CreateWindowExW(
+ # dwExStyle, lpClassName, lpWindowName, dwStyle
+ 0, class_atom, '', WS_OVERLAPPED,
+ # x, y, nWidth, nHeight
+ 0, 0, CW_USEDEFAULT, CW_USEDEFAULT,
+ # hWndParent, hMenu, hInstance, lpParam
+ None, None, wnd_class_ex.hInstance, None
+ )
+ if self._hwnd is None:
+ raise Exception('Could not get create window.')
+ win_api_defs.UpdateWindow(self._hwnd)
+
+ # load .ICO file for as balloon tip and tray icon
+ if app_icon:
+ icon_flags = LR_LOADFROMFILE | LR_DEFAULTSIZE
+ hicon = win_api_defs.LoadImageW(
+ None, app_icon, IMAGE_ICON, 0, 0, icon_flags
+ )
+
+ if hicon is None:
+ raise Exception('Could not load icon {}'.format(app_icon))
+ self._balloon_icon = self._hicon = hicon
+ else:
+ self._hicon = win_api_defs.LoadIconW(
+ None,
+ ctypes.cast(IDI_APPLICATION, win_api_defs.LPCWSTR)
+ )
+
+ # show the notification
+ self.notify(title, message, app_name)
+ if timeout:
+ time.sleep(timeout)
+
+ def __del__(self):
+ '''
+ Clean visible parts of the notification object, then free all resources
+ allocated for creating the nofitication Window and icon.
+ '''
+ self.remove_notify()
+ if self._hicon is not None:
+ win_api_defs.DestroyIcon(self._hicon)
+ if self._wnd_class_ex is not None:
+ win_api_defs.UnregisterClassW(
+ self._class_atom,
+ self._wnd_class_ex.hInstance
+ )
+ if self._hwnd is not None:
+ win_api_defs.DestroyWindow(self._hwnd)
+
+ def notify(self, title, message, app_name):
+ '''
+ Displays a balloon in the systray. Can be called multiple times
+ with different parameter values.
+ '''
+ # remove previous visible balloon tip nofitication if available
+ self.remove_notify()
+
+ # add icon and messages to window
+ hicon = self._hicon
+ flags = NIF_TIP | NIF_INFO
+ icon_flag = 0
+
+ if hicon is not None:
+ flags |= NIF_ICON
+
+ # if icon is default app's one, don't display it in message
+ if self._balloon_icon is not None:
+ icon_flag = NIIF_USER | NIIF_LARGE_ICON
+
+ notify_data = win_api_defs.get_NOTIFYICONDATAW(
+ 0, self._hwnd,
+ id(self), flags, 0, hicon, app_name, 0, 0, message,
+ NOTIFYICON_VERSION_4, title, icon_flag, win_api_defs.GUID(),
+ self._balloon_icon
+ )
+
+ self._notify_data = notify_data
+ if not win_api_defs.Shell_NotifyIconW(NIM_ADD, notify_data):
+ raise Exception('Shell_NotifyIconW failed.')
+ if not win_api_defs.Shell_NotifyIconW(NIM_SETVERSION,
+ notify_data):
+ raise Exception('Shell_NotifyIconW failed.')
+
+ def remove_notify(self):
+ '''
+ Removes the notify balloon, if displayed.
+ '''
+ if self._notify_data is not None:
+ win_api_defs.Shell_NotifyIconW(NIM_DELETE, self._notify_data)
+ self._notify_data = None
+
+
+def balloon_tip(**kwargs):
+ '''
+ Instance for balloon tip notification implementation.
+ '''
+ WindowsBalloonTip(**kwargs)
diff --git a/sbapp/plyer/platforms/win/libs/batterystatus.py b/sbapp/plyer/platforms/win/libs/batterystatus.py
new file mode 100644
index 00000000..1d8202dc
--- /dev/null
+++ b/sbapp/plyer/platforms/win/libs/batterystatus.py
@@ -0,0 +1,24 @@
+'''
+Module of Windows API helper for plyer.battery.
+'''
+
+__all__ = ('battery_status')
+
+
+import ctypes
+from plyer.platforms.win.libs import win_api_defs
+
+
+def battery_status():
+ '''
+ Implementation of Windows system power status API for plyer.battery.
+ '''
+
+ status = win_api_defs.SYSTEM_POWER_STATUS()
+ if not win_api_defs.GetSystemPowerStatus(ctypes.pointer(status)):
+ raise Exception('Could not get system power status.')
+
+ return dict(
+ (field, getattr(status, field))
+ for field, _ in status._fields_
+ )
diff --git a/sbapp/plyer/platforms/win/libs/wifi_defs.py b/sbapp/plyer/platforms/win/libs/wifi_defs.py
new file mode 100644
index 00000000..28998289
--- /dev/null
+++ b/sbapp/plyer/platforms/win/libs/wifi_defs.py
@@ -0,0 +1,572 @@
+'''
+Reference
+Methods, Structures and Documentation adapted from.
+https://msdn.microsoft.com/en-us/library/windows/desktop \
+/ms705945%28v=vs.85%29.aspx
+'''
+
+from ctypes import (
+ POINTER,
+ FormatError,
+ Structure,
+ addressof,
+ byref,
+ c_bool,
+ c_char,
+ c_ubyte,
+ c_uint,
+ c_ulong,
+ c_ushort,
+ c_void_p,
+ c_wchar,
+ pointer,
+ windll,
+)
+from ctypes.wintypes import DWORD, HANDLE, LPCWSTR, ULONG
+from sys import exit as sys_exit
+
+
+def customresize(array, new_size):
+ return (
+ array._type_ * new_size
+ ).from_address(
+ addressof(array)
+ )
+
+
+wlanapi = windll.LoadLibrary('wlanapi.dll')
+
+
+class GUID(Structure):
+ _fields_ = [
+ ('Data1', c_ulong),
+ ('Data2', c_ushort),
+ ('Data3', c_ushort),
+ ('Data4', c_ubyte * 8),
+ ]
+
+
+# The WLAN_INTERFACE_STATE enumerated type indicates the state of an interface.
+WLAN_INTERFACE_STATE = c_uint
+(wlan_interface_state_not_ready,
+ wlan_interface_state_connected,
+ wlan_interface_state_ad_hoc_network_formed,
+ wlan_interface_state_disconnecting,
+ wlan_interface_state_disconnected,
+ wlan_interface_state_associating,
+ wlan_interface_state_discovering,
+ wlan_interface_state_authenticating) = map(WLAN_INTERFACE_STATE,
+ range(0, 8))
+
+
+class WLAN_INTERFACE_INFO(Structure):
+ '''
+ The WLAN_INTERFACE_STATE enumerated type indicates the state of an
+ interface.
+ '''
+ _fields_ = [
+ ("InterfaceGuid", GUID),
+ ("strInterfaceDescription", c_wchar * 256),
+ ("isState", WLAN_INTERFACE_STATE)
+ ]
+
+
+class WLAN_INTERFACE_INFO_LIST(Structure):
+ '''
+ The WLAN_INTERFACE_INFO_LIST structure contains an array of NIC interface
+ information.
+ '''
+ _fields_ = [
+ ("NumberOfItems", DWORD),
+ ("Index", DWORD),
+ ("InterfaceInfo", WLAN_INTERFACE_INFO * 1)
+ ]
+
+
+DOT11_MAC_ADDRESS = c_ubyte * 6
+WLAN_MAX_PHY_TYPE_NUMBER = 0x8
+DOT11_SSID_MAX_LENGTH = 32
+WLAN_REASON_CODE = DWORD
+
+DOT11_BSS_TYPE = c_uint
+(dot11_BSS_type_infrastructure,
+ dot11_BSS_type_independent,
+ dot11_BSS_type_any) = map(DOT11_BSS_TYPE, range(1, 4))
+
+# The DOT11_PHY_TYPE enumeration defines an 802.11 PHY and media type.
+DOT11_PHY_TYPE = c_uint
+dot11_phy_type_unknown = 0
+dot11_phy_type_any = 0
+dot11_phy_type_fhss = 1
+dot11_phy_type_dsss = 2
+dot11_phy_type_irbaseband = 3
+dot11_phy_type_ofdm = 4
+dot11_phy_type_hrdsss = 5
+dot11_phy_type_erp = 6
+dot11_phy_type_ht = 7
+dot11_phy_type_IHV_start = 0x80000000
+dot11_phy_type_IHV_end = 0xffffffff
+
+# The DOT11_AUTH_ALGORITHM enumerated type defines a wireless
+# LAN authentication algorithm.
+DOT11_AUTH_ALGORITHM = c_uint
+DOT11_AUTH_ALGO_80211_OPEN = 1
+DOT11_AUTH_ALGO_80211_SHARED_KEY = 2
+DOT11_AUTH_ALGO_WPA = 3
+DOT11_AUTH_ALGO_WPA_PSK = 4
+DOT11_AUTH_ALGO_WPA_NONE = 5
+DOT11_AUTH_ALGO_RSNA = 6
+DOT11_AUTH_ALGO_RSNA_PSK = 7
+DOT11_AUTH_ALGO_IHV_START = 0x80000000
+DOT11_AUTH_ALGO_IHV_END = 0xffffffff
+
+# The DOT11_CIPHER_ALGORITHM enumerated type defines a cipher
+# algorithm for data encryption and decryption.
+DOT11_CIPHER_ALGORITHM = c_uint
+DOT11_CIPHER_ALGO_NONE = 0x00
+DOT11_CIPHER_ALGO_WEP40 = 0x01
+DOT11_CIPHER_ALGO_TKIP = 0x02
+DOT11_CIPHER_ALGO_CCMP = 0x04
+DOT11_CIPHER_ALGO_WEP104 = 0x05
+DOT11_CIPHER_ALGO_WPA_USE_GROUP = 0x100
+DOT11_CIPHER_ALGO_RSN_USE_GROUP = 0x100
+DOT11_CIPHER_ALGO_WEP = 0x101
+DOT11_CIPHER_ALGO_IHV_START = 0x80000000
+DOT11_CIPHER_ALGO_IHV_END = 0xffffffff
+
+
+class DOT11_SSID(Structure):
+ '''
+ A DOT11_SSID structure contains the SSID of an interface
+ '''
+ _fields_ = [
+ ("SSIDLength", c_ulong),
+ ("SSID", c_char * DOT11_SSID_MAX_LENGTH)
+ ]
+
+
+# Enumerated type to define the code of connection.
+WLAN_CONNECTION_MODE = c_uint
+(wlan_connection_mode_profile,
+ wlan_connection_mode_temporary_profile,
+ wlan_connection_mode_discovery_secure,
+ wlan_connection_mode_discovery_unsecure,
+ wlan_connection_mode_auto,
+ wlan_connection_mode_invalid) = map(WLAN_CONNECTION_MODE, range(0, 6))
+
+
+class NDIS_OBJECT_HEADER(Structure):
+ '''
+ This Structure packages the object type, version, and size information
+ that is required in many NDIS (Netword Driver interface Specification)
+ Structures.
+ '''
+ _fields_ = [
+ ("Type", c_char),
+ ("Revision", c_char),
+ ("Size", c_ushort)]
+
+
+class DOT11_BSSID_LIST(Structure):
+ '''
+ The DOT11_BSSID_LIST structure contains a list of basic service set (BSS)
+ identifiers.
+ '''
+ _fields_ = [
+ ("Header", NDIS_OBJECT_HEADER),
+ ("uNumOfEntries", ULONG),
+ ("uTotalNumOfEntries", ULONG),
+ ("BSSIDs", DOT11_MAC_ADDRESS * 1)
+ ]
+
+
+class WLAN_CONNECTION_PARAMETERS(Structure):
+ '''
+ The WLAN_CONNECTION_PARAMETERS structure specifies the parameters used when
+ using the WlanConnect function.
+ '''
+ _fields_ = [
+ ("wlanConnectionMode", WLAN_CONNECTION_MODE),
+ ("strProfile", LPCWSTR),
+ ("pDot11Ssid", POINTER(DOT11_SSID)),
+ ("pDesiredBssidList", POINTER(DOT11_BSSID_LIST)),
+ ("dot11BssType", DOT11_BSS_TYPE),
+ ("dwFlags", DWORD)]
+
+
+# The `WlanConnect` attempts to connect to a specific network.
+WlanConnect = wlanapi.WlanConnect
+WlanConnect.argtypes = (HANDLE,
+ POINTER(GUID),
+ POINTER(WLAN_CONNECTION_PARAMETERS),
+ c_void_p)
+WlanConnect.restype = DWORD
+
+# The `WlanDisconnect` method disconnects an interface from its
+# current network.
+WlanDisconnect = wlanapi.WlanDisconnect
+WlanDisconnect.argtypes = (HANDLE,
+ POINTER(GUID),
+ c_void_p)
+WlanDisconnect.restype = DWORD
+
+# Opens a connection to the server.
+WlanOpenHandle = wlanapi.WlanOpenHandle
+WlanOpenHandle.argtypes = (DWORD, c_void_p, POINTER(DWORD), POINTER(HANDLE))
+WlanOpenHandle.restype = DWORD
+
+# The WlanCloseHandle method closes the connection to the server.
+WlanCloseHandle = wlanapi.WlanCloseHandle
+WlanCloseHandle.argtypes = (HANDLE, c_void_p)
+WlanCloseHandle.restype = DWORD
+
+
+class WLAN_AVAILABLE_NETWORK(Structure):
+ '''
+ The WLAN_INTERFACE_INFO structure contains information about a wireless
+ LAN interface.
+ '''
+ _fields_ = [
+ ("ProfileName", c_wchar * 256),
+ ("dot11Ssid", DOT11_SSID),
+ ("dot11BssType", DOT11_BSS_TYPE),
+ ("NumberOfBssids", c_ulong),
+ ("NetworkConnectable", c_bool),
+ ("wlanNotConnectableReason", WLAN_REASON_CODE),
+ ("NumberOfPhyTypes", c_ulong),
+ ("dot11PhyTypes", DOT11_PHY_TYPE * WLAN_MAX_PHY_TYPE_NUMBER),
+ ("MorePhyTypes", c_bool),
+ ("wlanSignalQuality", c_ulong),
+ ("SecurityEnabled", c_bool),
+ ("dot11DefaultAuthAlgorithm", DOT11_AUTH_ALGORITHM),
+ ("dot11DefaultCipherAlgorithm", DOT11_CIPHER_ALGORITHM),
+ ("Flags", DWORD),
+ ("Reserved", DWORD)]
+
+
+class WLAN_AVAILABLE_NETWORK_LIST(Structure):
+ '''
+ The WLAN_INTERFACE_INFO_LIST structure contains an array of NIC
+ interface information.
+ '''
+ _fields_ = [
+ ("NumberOfItems", DWORD),
+ ("Index", DWORD),
+ ("Network", WLAN_AVAILABLE_NETWORK * 1)]
+
+
+# The WlanEnumInterfaces function enumerates all of the wireless LAN interfaces
+# currently enabled on the local computer.
+WlanEnumInterfaces = wlanapi.WlanEnumInterfaces
+WlanEnumInterfaces.argtypes = (HANDLE,
+ c_void_p,
+ POINTER(POINTER(WLAN_INTERFACE_INFO_LIST)))
+WlanEnumInterfaces.restype = DWORD
+
+# The WlanGetAvailableNetworkList function retrieves the list of available
+# networks on a wireless LAN interface.
+WlanGetAvailableNetworkList = wlanapi.WlanGetAvailableNetworkList
+WlanGetAvailableNetworkList.argtypes = (HANDLE,
+ POINTER(GUID),
+ DWORD,
+ c_void_p,
+ POINTER(POINTER(
+ WLAN_AVAILABLE_NETWORK_LIST)))
+WlanGetAvailableNetworkList.restype = DWORD
+
+# The WlanFreeMemory function frees memory. Any memory returned from Native
+# Wifi functions must be freed.
+WlanFreeMemory = wlanapi.WlanFreeMemory
+WlanFreeMemory.argtypes = [c_void_p]
+
+wireless_interfaces = None
+available = None
+_dict = {}
+
+# Private methods.
+
+
+def _connect(network, parameters):
+ '''
+ Attempts to connect to a specific network.
+ '''
+ global _dict
+ wireless_interface = _dict[network]
+
+ wcp = WLAN_CONNECTION_PARAMETERS()
+ connection_mode = parameters['connection_mode']
+ wcp.wlanConnectionMode = WLAN_CONNECTION_MODE(connection_mode)
+
+ if connection_mode == 0 or connection_mode == 1:
+ wcp.strProfile = LPCWSTR(parameters["profile"])
+ else:
+ wcp.strProfile = None
+
+ dot11Ssid = DOT11_SSID()
+ try:
+ dot11Ssid.SSID = parameters["ssid"]
+ dot11Ssid.SSIDLength = len(parameters["ssid"])
+ except KeyError:
+ dot11Ssid.SSID = network
+ dot11Ssid.SSIDLength = len(network)
+ wcp.pDot11Ssid = pointer(dot11Ssid)
+
+ dot11bssid = DOT11_BSSID_LIST()
+ bssid = parameters["bssidList"]
+ dot11bssid.Header = bssid['Header']
+ dot11bssid.uNumOfEntries = bssid['uNumOfEntries']
+ dot11bssid.uTotalNumOfEntries = bssid['uTotalNumOfEntries']
+ dot11bssid.BSSIDs = bssid['BSSIDs']
+
+ wcp.pDesiredBssidList = pointer(dot11bssid)
+
+ bssType = parameters["bssType"]
+ wcp.dot11BssType = DOT11_BSS_TYPE(bssType)
+
+ wcp.dwFlags = DWORD(parameters["flags"])
+
+ NegotiatedVersion = DWORD()
+ ClientHandle = HANDLE()
+
+ wlan = WlanOpenHandle(1,
+ None,
+ byref(NegotiatedVersion),
+ byref(ClientHandle))
+ if wlan:
+ sys_exit(FormatError(wlan))
+ pInterfaceList = pointer(WLAN_INTERFACE_INFO_LIST())
+ wlan = WlanEnumInterfaces(ClientHandle, None, byref(pInterfaceList))
+ if wlan:
+ sys_exit(FormatError(wlan))
+
+ try:
+ wlan = WlanConnect(ClientHandle,
+ wireless_interface,
+ wcp,
+ None)
+ if wlan:
+ sys_exit(FormatError(wlan))
+ WlanCloseHandle(ClientHandle, None)
+ finally:
+ WlanFreeMemory(pInterfaceList)
+
+
+def _disconnect():
+ '''
+ To disconnect an interface form the current network.
+ '''
+ NegotiatedVersion = DWORD()
+ ClientHandle = HANDLE()
+
+ wlan = WlanOpenHandle(
+ 1,
+ None,
+ byref(NegotiatedVersion),
+ byref(ClientHandle)
+ )
+ if wlan:
+ sys_exit(FormatError(wlan))
+
+ pInterfaceList = pointer(WLAN_INTERFACE_INFO_LIST())
+
+ wlan = WlanEnumInterfaces(ClientHandle, None, byref(pInterfaceList))
+ if wlan:
+ sys_exit(FormatError(wlan))
+
+ result = None
+ try:
+ ifaces = customresize(
+ pInterfaceList.contents.InterfaceInfo,
+ pInterfaceList.contents.NumberOfItems
+ )
+
+ # find each available network for each interface
+ for iface in ifaces:
+ wlan = WlanDisconnect(
+ ClientHandle,
+ byref(iface.InterfaceGuid),
+ None
+ )
+ if wlan:
+ sys_exit(FormatError(wlan))
+ WlanCloseHandle(ClientHandle, None)
+
+ finally:
+ WlanFreeMemory(pInterfaceList)
+ result = get_available_wifi()
+
+ return result
+
+
+def _start_scanning():
+ '''
+ Private method for scanning and returns the available devices.
+ '''
+ global available
+ global wireless_interfaces
+ NegotiatedVersion = DWORD()
+ ClientHandle = HANDLE()
+
+ wlan = WlanOpenHandle(
+ 1,
+ None,
+ byref(NegotiatedVersion),
+ byref(ClientHandle)
+ )
+
+ if wlan:
+ sys_exit(FormatError(wlan))
+
+ # find all wireless network interfaces
+ pInterfaceList = pointer(WLAN_INTERFACE_INFO_LIST())
+ wlan = WlanEnumInterfaces(ClientHandle, None, byref(pInterfaceList))
+ if wlan:
+ sys_exit(FormatError(wlan))
+
+ result = None
+ try:
+ ifaces = customresize(
+ pInterfaceList.contents.InterfaceInfo,
+ pInterfaceList.contents.NumberOfItems
+ )
+
+ # find each available network for each interface
+ wireless_interfaces = ifaces
+ for iface in ifaces:
+ pAvailableNetworkList = pointer(WLAN_AVAILABLE_NETWORK_LIST())
+ wlan = WlanGetAvailableNetworkList(
+ ClientHandle,
+ byref(iface.InterfaceGuid),
+ 0,
+ None,
+ byref(pAvailableNetworkList)
+ )
+
+ if wlan:
+ sys_exit(FormatError(wlan))
+
+ try:
+ avail_net_list = pAvailableNetworkList.contents
+ networks = customresize(
+ avail_net_list.Network,
+ avail_net_list.NumberOfItems
+ )
+
+ # Assigning the value of networks to the global variable
+ # `available`, so it could be used in other methods.
+ available = networks
+ _make_dict()
+ wlan = WlanDisconnect(
+ ClientHandle,
+ byref(iface.InterfaceGuid),
+ None
+ )
+
+ if wlan:
+ sys_exit(FormatError(wlan))
+ WlanCloseHandle(ClientHandle, None)
+
+ finally:
+ WlanFreeMemory(pAvailableNetworkList)
+
+ finally:
+ WlanFreeMemory(pInterfaceList)
+ result = get_available_wifi()
+
+ return result
+
+
+def _get_network_info(name):
+ '''
+ returns the list of the network selected in a dict.
+ '''
+ global available
+ global _dict
+
+ net = _dict[name]
+ dot11BssType = net.dot11BssType
+ dot11DefaultAuthAlgorithm = net.dot11DefaultAuthAlgorithm
+ dot11DefaultCipherAlgorithm = net.dot11DefaultCipherAlgorithm
+ dot11PhyTypes = net.dot11PhyTypes[0]
+ dot11Ssid = net.dot11Ssid
+ wlanNotConnectableReason = net.wlanNotConnectableReason
+ wlanSignalQuality = net.wlanSignalQuality
+ return {"dot11BssType": dot11BssType,
+ "dot11DefaultAuthAlgorithm": dot11DefaultAuthAlgorithm,
+ "dot11DefaultCipherAlgorithm": dot11DefaultCipherAlgorithm,
+ "dot11PhyTypes": dot11PhyTypes,
+ "SSID": dot11Ssid.SSID,
+ "SSIDLength": dot11Ssid.SSIDLength,
+ "wlanNotConnectableReason": wlanNotConnectableReason,
+ "wlanSignalQuality": wlanSignalQuality}
+
+
+def _make_dict():
+ '''
+ Prepares a dict so it could store network information.
+ '''
+ global available
+ global _dict
+ _dict = {}
+ for network in available:
+ _dict[network.dot11Ssid.SSID.decode('utf-8')] = network
+
+
+def _get_available_wifi():
+ '''
+ returns the available wifi networks.
+ '''
+ global _dict
+ return _dict
+
+
+def _is_enabled():
+ '''
+ Reason for returning true is explained in widi facade.
+ /plyer/facades/wifi.py
+ '''
+ return True
+
+# public methods.
+
+
+def is_enabled():
+ '''
+ calls private method `_is_enabled` and returns the result.
+ '''
+ return _is_enabled()
+
+
+def connect(network, parameters):
+ '''
+ Connect to a network with following parameters.
+ '''
+ _connect(network=network, parameters=parameters)
+
+
+def disconnect():
+ '''
+ Disconnect from a network.
+ '''
+ _disconnect()
+
+
+def start_scanning():
+ '''
+ Start scanning for available wifi networks available.
+ '''
+ return _start_scanning()
+
+
+def get_network_info(name):
+ '''
+ return the wifi network info.
+ '''
+ return _get_network_info(name=name)
+
+
+def get_available_wifi():
+ '''
+ return the available wifi networks available
+ '''
+ return _get_available_wifi()
diff --git a/sbapp/plyer/platforms/win/libs/win_api_defs.py b/sbapp/plyer/platforms/win/libs/win_api_defs.py
new file mode 100755
index 00000000..dcf116b6
--- /dev/null
+++ b/sbapp/plyer/platforms/win/libs/win_api_defs.py
@@ -0,0 +1,250 @@
+''' Defines ctypes windows api.
+'''
+
+__all__ = ('GUID', 'get_DLLVERSIONINFO', 'MAKEDLLVERULL',
+ 'get_NOTIFYICONDATAW', 'CreateWindowExW', 'WindowProc',
+ 'DefWindowProcW', 'get_WNDCLASSEXW', 'GetModuleHandleW',
+ 'RegisterClassExW', 'UpdateWindow', 'LoadImageW',
+ 'Shell_NotifyIconW', 'DestroyIcon', 'UnregisterClassW',
+ 'DestroyWindow', 'LoadIconW', 'get_PATH')
+
+import ctypes
+from ctypes import Structure, windll, sizeof, POINTER, WINFUNCTYPE
+from ctypes.wintypes import (
+ DWORD, HICON, HWND, UINT, WCHAR, WORD, BYTE,
+ LPCWSTR, INT, LPVOID, HINSTANCE, HMENU, LPARAM,
+ WPARAM, HBRUSH, HMODULE, ATOM, BOOL, HANDLE
+)
+LRESULT = LPARAM
+HRESULT = HANDLE
+HCURSOR = HICON
+
+
+class GUID(Structure):
+ _fields_ = [
+ ('Data1', DWORD),
+ ('Data2', WORD),
+ ('Data3', WORD),
+ ('Data4', BYTE * 8)
+ ]
+
+
+class DLLVERSIONINFO(Structure):
+ _fields_ = [
+ ('cbSize', DWORD),
+ ('dwMajorVersion', DWORD),
+ ('dwMinorVersion', DWORD),
+ ('dwBuildNumber', DWORD),
+ ('dwPlatformID', DWORD),
+ ]
+
+
+def get_DLLVERSIONINFO(*largs):
+ version_info = DLLVERSIONINFO(*largs)
+ version_info.cbSize = sizeof(DLLVERSIONINFO)
+ return version_info
+
+
+def MAKEDLLVERULL(major, minor, build, sp):
+ return (major << 48) | (minor << 32) | (build << 16) | sp
+
+
+NOTIFYICONDATAW_fields = [
+ ("cbSize", DWORD),
+ ("hWnd", HWND),
+ ("uID", UINT),
+ ("uFlags", UINT),
+ ("uCallbackMessage", UINT),
+ ("hIcon", HICON),
+ ("szTip", WCHAR * 128),
+ ("dwState", DWORD),
+ ("dwStateMask", DWORD),
+ ("szInfo", WCHAR * 256),
+ ("uVersion", UINT),
+ ("szInfoTitle", WCHAR * 64),
+ ("dwInfoFlags", DWORD),
+ ("guidItem", GUID),
+ ("hBalloonIcon", HICON),
+]
+
+
+class NOTIFYICONDATAW(Structure):
+ _fields_ = NOTIFYICONDATAW_fields[:]
+
+
+class NOTIFYICONDATAW_V3(Structure):
+ _fields_ = NOTIFYICONDATAW_fields[:-1]
+
+
+class NOTIFYICONDATAW_V2(Structure):
+ _fields_ = NOTIFYICONDATAW_fields[:-2]
+
+
+class NOTIFYICONDATAW_V1(Structure):
+ _fields_ = NOTIFYICONDATAW_fields[:6]
+
+
+NOTIFYICONDATA_V3_SIZE = sizeof(NOTIFYICONDATAW_V3)
+NOTIFYICONDATA_V2_SIZE = sizeof(NOTIFYICONDATAW_V2)
+NOTIFYICONDATA_V1_SIZE = sizeof(NOTIFYICONDATAW_V1)
+
+
+def get_NOTIFYICONDATAW(*largs):
+ notify_data = NOTIFYICONDATAW(*largs)
+
+ # get shell32 version to find correct NOTIFYICONDATAW size
+ DllGetVersion = windll.Shell32.DllGetVersion
+ DllGetVersion.argtypes = [POINTER(DLLVERSIONINFO)]
+ DllGetVersion.restype = HRESULT
+
+ version = get_DLLVERSIONINFO()
+ if DllGetVersion(version):
+ raise Exception('Cannot get Windows version numbers.')
+ v = MAKEDLLVERULL(version.dwMajorVersion, version.dwMinorVersion,
+ version.dwBuildNumber, version.dwPlatformID)
+
+ # from the version info find the NOTIFYICONDATA size
+ if v >= MAKEDLLVERULL(6, 0, 6, 0):
+ notify_data.cbSize = sizeof(NOTIFYICONDATAW)
+ elif v >= MAKEDLLVERULL(6, 0, 0, 0):
+ notify_data.cbSize = NOTIFYICONDATA_V3_SIZE
+ elif v >= MAKEDLLVERULL(5, 0, 0, 0):
+ notify_data.cbSize = NOTIFYICONDATA_V2_SIZE
+ else:
+ notify_data.cbSize = NOTIFYICONDATA_V1_SIZE
+ return notify_data
+
+
+CreateWindowExW = windll.User32.CreateWindowExW
+CreateWindowExW.argtypes = [DWORD, ATOM, LPCWSTR, DWORD, INT, INT, INT, INT,
+ HWND, HMENU, HINSTANCE, LPVOID]
+CreateWindowExW.restype = HWND
+
+GetModuleHandleW = windll.Kernel32.GetModuleHandleW
+GetModuleHandleW.argtypes = [LPCWSTR]
+GetModuleHandleW.restype = HMODULE
+
+WindowProc = WINFUNCTYPE(LRESULT, HWND, UINT, WPARAM, LPARAM)
+DefWindowProcW = windll.User32.DefWindowProcW
+DefWindowProcW.argtypes = [HWND, UINT, WPARAM, LPARAM]
+DefWindowProcW.restype = LRESULT
+
+
+class WNDCLASSEXW(Structure):
+ _fields_ = [
+ ('cbSize', UINT),
+ ('style', UINT),
+ ('lpfnWndProc', WindowProc),
+ ('cbClsExtra', INT),
+ ('cbWndExtra', INT),
+ ('hInstance', HINSTANCE),
+ ('hIcon', HICON),
+ ('hCursor', HCURSOR),
+ ('hbrBackground', HBRUSH),
+ ('lpszMenuName', LPCWSTR),
+ ('lpszClassName', LPCWSTR),
+ ('hIconSm', HICON),
+ ]
+
+
+def get_WNDCLASSEXW(*largs):
+ wnd_class = WNDCLASSEXW(*largs)
+ wnd_class.cbSize = sizeof(WNDCLASSEXW)
+ return wnd_class
+
+
+RegisterClassExW = windll.User32.RegisterClassExW
+RegisterClassExW.argtypes = [POINTER(WNDCLASSEXW)]
+RegisterClassExW.restype = ATOM
+
+UpdateWindow = windll.User32.UpdateWindow
+UpdateWindow.argtypes = [HWND]
+UpdateWindow.restype = BOOL
+
+LoadImageW = windll.User32.LoadImageW
+LoadImageW.argtypes = [HINSTANCE, LPCWSTR, UINT, INT, INT, UINT]
+LoadImageW.restype = HANDLE
+
+Shell_NotifyIconW = windll.Shell32.Shell_NotifyIconW
+Shell_NotifyIconW.argtypes = [DWORD, POINTER(NOTIFYICONDATAW)]
+Shell_NotifyIconW.restype = BOOL
+
+DestroyIcon = windll.User32.DestroyIcon
+DestroyIcon.argtypes = [HICON]
+DestroyIcon.restype = BOOL
+
+UnregisterClassW = windll.User32.UnregisterClassW
+UnregisterClassW.argtypes = [ATOM, HINSTANCE]
+UnregisterClassW.restype = BOOL
+
+DestroyWindow = windll.User32.DestroyWindow
+DestroyWindow.argtypes = [HWND]
+DestroyWindow.restype = BOOL
+
+LoadIconW = windll.User32.LoadIconW
+LoadIconW.argtypes = [HINSTANCE, LPCWSTR]
+LoadIconW.restype = HICON
+
+
+class SYSTEM_POWER_STATUS(Structure):
+ _fields_ = [
+ ('ACLineStatus', BYTE),
+ ('BatteryFlag', BYTE),
+ ('BatteryLifePercent', BYTE),
+ ('Reserved1', BYTE),
+ ('BatteryLifeTime', DWORD),
+ ('BatteryFullLifeTime', DWORD),
+ ]
+
+
+SystemPowerStatusP = POINTER(SYSTEM_POWER_STATUS)
+
+GetSystemPowerStatus = windll.kernel32.GetSystemPowerStatus
+GetSystemPowerStatus.argtypes = [SystemPowerStatusP]
+GetSystemPowerStatus.restype = BOOL
+
+
+class GUID_(Structure):
+ _fields_ = [
+ ('Data1', DWORD),
+ ('Data2', WORD),
+ ('Data3', WORD),
+ ('Data4', BYTE * 8)
+ ]
+
+ def __init__(self, uuid_):
+ Structure.__init__(self)
+ self.Data1, self.Data2, self.Data3, self.Data4[0], self.Data4[1], rest\
+ = uuid_.fields
+ for i in range(2, 8):
+ self.Data4[i] = rest >> (8 - i - 1) * 8 & 0xff
+
+
+_CoTaskMemFree = windll.ole32.CoTaskMemFree
+_CoTaskMemFree.restype = None
+_CoTaskMemFree.argtypes = [ctypes.c_void_p]
+
+_SHGetKnownFolderPath = windll.shell32.SHGetKnownFolderPath
+_SHGetKnownFolderPath.argtypes = [
+ POINTER(GUID_),
+ DWORD,
+ HANDLE,
+ POINTER(ctypes.c_wchar_p)
+]
+
+
+class PathNotFoundException(Exception):
+ pass
+
+
+def get_PATH(folderid):
+ fid = GUID_(folderid)
+ pPath = ctypes.c_wchar_p()
+ S_OK = 0
+ Result = _SHGetKnownFolderPath(ctypes.byref(fid),
+ 0, None, ctypes.byref(pPath))
+ if Result != S_OK:
+ raise PathNotFoundException()
+ path = pPath.value
+ _CoTaskMemFree(pPath)
+ return path
diff --git a/sbapp/plyer/platforms/win/notification.py b/sbapp/plyer/platforms/win/notification.py
new file mode 100644
index 00000000..bac381ba
--- /dev/null
+++ b/sbapp/plyer/platforms/win/notification.py
@@ -0,0 +1,24 @@
+'''
+Module of Windows API for plyer.notification.
+'''
+
+from threading import Thread as thread
+
+from plyer.facades import Notification
+from plyer.platforms.win.libs.balloontip import balloon_tip
+
+
+class WindowsNotification(Notification):
+ '''
+ Implementation of Windows notification/balloon API.
+ '''
+
+ def _notify(self, **kwargs):
+ thread(target=balloon_tip, kwargs=kwargs).start()
+
+
+def instance():
+ '''
+ Instance for facade proxy.
+ '''
+ return WindowsNotification()
diff --git a/sbapp/plyer/platforms/win/screenshot.py b/sbapp/plyer/platforms/win/screenshot.py
new file mode 100644
index 00000000..b04eab57
--- /dev/null
+++ b/sbapp/plyer/platforms/win/screenshot.py
@@ -0,0 +1,107 @@
+'''
+https://docs.microsoft.com/en-us/windows/desktop/api/wingdi/nf-wingdi-bitblt
+https://www.bugs.python.org/issue33656
+'''
+
+from os import getpid
+from os.path import join
+from ctypes import windll, c_int, addressof
+
+from win32gui import GetDesktopWindow, GetWindowDC
+from win32api import GetSystemMetrics
+from win32ui import CreateDCFromHandle, CreateBitmap
+from win32con import (
+ SM_CXVIRTUALSCREEN,
+ SM_CYVIRTUALSCREEN,
+ SM_XVIRTUALSCREEN,
+ SM_YVIRTUALSCREEN,
+ SRCCOPY
+)
+
+from plyer.facades import Screenshot
+from plyer.platforms.win.storagepath import WinStoragePath
+
+
+class WinScreenshot(Screenshot):
+ def __init__(self, file_path=None):
+ default_path = join(
+ WinStoragePath().get_pictures_dir(), 'screenshot.bmp'
+ )
+ super().__init__(file_path or default_path)
+
+ def _set_dpi_aware(self, value):
+ try:
+ windll.shcore.SetProcessDpiAwareness(value)
+ except (AttributeError, OSError):
+ print('Could not set DPI awareness.')
+
+ def _dpi_aware(self):
+ # make backup of DPI awareness value in case a user does not want
+ # to use it, otherwise we'll cripple user's runtime
+ try:
+ process_handle = windll.kernel32.OpenProcess(
+ 0, # no permissions
+ False, # bInheritHandle
+ getpid()
+ )
+ aware = c_int()
+ windll.shcore.GetProcessDpiAwareness(
+ process_handle,
+ addressof(aware)
+ )
+ finally:
+ # always close the handle!
+ windll.kernel32.CloseHandle(process_handle)
+ return bool(aware)
+
+ def _capture(self):
+ # make sure the process is DPI aware, otherwise the image
+ # is only a part of the full monitor content
+ # (necessary only on Win 8.1+)
+ old_awareness = self._dpi_aware()
+ self._set_dpi_aware(True)
+
+ # get width and height of current monitor
+ width = GetSystemMetrics(SM_CXVIRTUALSCREEN)
+ height = GetSystemMetrics(SM_CYVIRTUALSCREEN)
+
+ # get 'desktop' window handle
+ handle_desktop = GetDesktopWindow()
+
+ # get window graphic context handle
+ handle_context = GetWindowDC(handle_desktop)
+
+ # create device context
+ dev_ctx = CreateDCFromHandle(handle_context)
+
+ # create destination for original device context
+ dest_ctx = dev_ctx.CreateCompatibleDC()
+
+ # create bitmap compatible with desktop window device
+ bmp = CreateBitmap()
+ bmp.CreateCompatibleBitmap(dev_ctx, width, height)
+
+ # select bitmap into destination device
+ dest_ctx.SelectObject(bmp)
+
+ # populate selected bitmap in destination device
+ dest_ctx.BitBlt(
+ (0, 0), # start point (x, y)
+ (width, height), # size of rectangle
+ dev_ctx, # source device
+ (
+ GetSystemMetrics(SM_XVIRTUALSCREEN),
+ GetSystemMetrics(SM_YVIRTUALSCREEN)
+ ), # source rectangle, can be different monitor
+ SRCCOPY # copy directly without filters
+ )
+
+ # save bitmap to file
+ bmp.SaveBitmapFile(dest_ctx, self.file_path)
+
+ # return to the original state
+ self._set_dpi_aware(old_awareness)
+
+
+def instance():
+ return WinScreenshot()
diff --git a/sbapp/plyer/platforms/win/storagepath.py b/sbapp/plyer/platforms/win/storagepath.py
new file mode 100755
index 00000000..fd905d8a
--- /dev/null
+++ b/sbapp/plyer/platforms/win/storagepath.py
@@ -0,0 +1,53 @@
+'''
+Windows Storage Path
+--------------------
+'''
+
+from plyer.facades import StoragePath
+from os.path import expanduser
+from plyer.platforms.win.libs.win_api_defs import get_PATH
+from uuid import UUID
+
+
+class WinStoragePath(StoragePath):
+
+ def _get_home_dir(self):
+ return expanduser('~')
+
+ def _get_external_storage_dir(self):
+ '''
+ To do.
+ '''
+ return "Method not implemented for current platform."
+
+ def _get_root_dir(self):
+ folderid = UUID('{F38BF404-1D43-42F2-9305-67DE0B28FC23}')
+ return get_PATH(folderid)
+
+ def _get_documents_dir(self):
+ folderid = UUID('{FDD39AD0-238F-46AF-ADB4-6C85480369C7}')
+ return get_PATH(folderid)
+
+ def _get_downloads_dir(self):
+ folderid = UUID('{374DE290-123F-4565-9164-39C4925E467B}')
+ return get_PATH(folderid)
+
+ def _get_videos_dir(self):
+ folderid = UUID('{18989B1D-99B5-455B-841C-AB7C74E4DDFC}')
+ return get_PATH(folderid)
+
+ def _get_music_dir(self):
+ folderid = UUID('{4BD8D571-6D19-48D3-BE97-422220080E43}')
+ return get_PATH(folderid)
+
+ def _get_pictures_dir(self):
+ folderid = UUID('{33E28130-4E1E-4676-835A-98395C3BC3BB}')
+ return get_PATH(folderid)
+
+ def _get_application_dir(self):
+ folderid = UUID('{905E63B6-C1BF-494E-B29C-65B732D3D21A}')
+ return get_PATH(folderid)
+
+
+def instance():
+ return WinStoragePath()
diff --git a/sbapp/plyer/platforms/win/tts.py b/sbapp/plyer/platforms/win/tts.py
new file mode 100644
index 00000000..e2539c3e
--- /dev/null
+++ b/sbapp/plyer/platforms/win/tts.py
@@ -0,0 +1,16 @@
+import subprocess
+from plyer.facades import TTS
+from plyer.utils import whereis_exe
+
+
+class EspeakTextToSpeech(TTS):
+ ''' Speaks using the espeak program
+ '''
+ def _speak(self, **kwargs):
+ subprocess.call(["espeak", kwargs.get('message')])
+
+
+def instance():
+ if whereis_exe('espeak.exe'):
+ return EspeakTextToSpeech()
+ return TTS()
diff --git a/sbapp/plyer/platforms/win/uniqueid.py b/sbapp/plyer/platforms/win/uniqueid.py
new file mode 100644
index 00000000..6d423911
--- /dev/null
+++ b/sbapp/plyer/platforms/win/uniqueid.py
@@ -0,0 +1,36 @@
+'''
+Module of Windows API for plyer.uniqueid.
+'''
+
+try:
+ import _winreg as regedit
+except ImportError:
+ try:
+ import winreg as regedit
+ except ImportError:
+ raise NotImplementedError()
+
+from plyer.facades import UniqueID
+
+
+class WinUniqueID(UniqueID):
+ '''
+ Implementation of Windows battery API.
+ '''
+
+ def _get_uid(self):
+ # Win XP+, REG QUERY KEY /V VALUE, case-insensitive
+ handle = regedit.OpenKey(
+ regedit.HKEY_LOCAL_MACHINE,
+ r"SOFTWARE\\Microsoft\\Cryptography", 0,
+ regedit.KEY_READ | regedit.KEY_WOW64_64KEY
+ )
+ value, _ = regedit.QueryValueEx(handle, "MachineGuid")
+ return value
+
+
+def instance():
+ '''
+ Instance for facade proxy.
+ '''
+ return WinUniqueID()
diff --git a/sbapp/plyer/platforms/win/wifi.py b/sbapp/plyer/platforms/win/wifi.py
new file mode 100644
index 00000000..68efb2ff
--- /dev/null
+++ b/sbapp/plyer/platforms/win/wifi.py
@@ -0,0 +1,121 @@
+import plyer.platforms.win.libs.wifi_defs as wifi_lib
+from plyer.facades import Wifi
+
+
+class WindowWifi(Wifi):
+
+ names = {}
+
+ def _is_enabled(self):
+ '''
+ TODO: Implement this in future
+ Couldn't find a nice implementation for this although
+ NetworkInformation class could be used but ctypes doesn't supports
+ class yet. It should look something like this.
+
+ for item in NetworkInformation.getConnectionProfiles():
+ if item.IsWlanConnectionProfile:
+ adapter_id = item.NetworkAdapter.NetworkAdapterId
+ for item in NetworkInformation.GetLanIdentifiers():
+ if item.NetworkAdapterId == adapter_id:
+ is_wifi_enabled = True
+ return True/False
+
+ Returning True for now to make it work.
+ '''
+ return True
+
+ def _get_network_info(self, name):
+ '''
+ Returns all the network information.
+ '''
+ return wifi_lib.get_network_info(name)
+
+ def _start_scanning(self):
+ '''
+ Starts scanning for available Wi-Fi networks and returns the available,
+ devices.
+ '''
+ if self._is_enabled():
+ self.names = wifi_lib.start_scanning()
+ else:
+ raise Exception('Wifi not Enabled.')
+
+ def _get_available_wifi(self):
+ '''
+ Returns the name of available networks.
+ '''
+ return wifi_lib.get_available_wifi()
+
+ def _connect(self, network, parameters):
+ '''
+ Expects 2 parameters:
+ - name/ssid of the network.
+ - parameters: dict type
+ - connection_mode:
+ `https://msdn.microsoft.com/en-us/library/windows/desktop/
+ ms706844(v=vs.85).aspx`
+
+ :between range [0, 5]
+ - wlan_connection_mode_profile,
+ wlan_connection_mode_temporary_profile,
+ wlan_connection_mode_discovery_secure,
+ wlan_connection_mode_discovery_unsecure,
+ wlan_connection_mode_auto,
+ wlan_connection_mode_invalid.
+ - profile:
+ if wlanConnectionMode = wlan_connection_mode_profile
+ then profile = ssid
+ if wlanConnectionMode = wlan_connection_mode_temporary_profile
+ then profile = XML representation of the profile used for the
+ connection
+ if wlanConnectionMode = wlan_connection_mode_discovery_secure
+ or
+ wlan_connection_mode_discovery_unsecure
+ then profile = None
+ - ssid: optional (as network name and ssid are same)
+ - bssidList
+ `https://msdn.microsoft.com/en-us/library/windows/desktop/
+ ms705996(v=vs.85).aspx`
+ - Header
+ structure that contains the type, version, and, size
+ information
+ of an NDIS structure.
+ - Type: NDSI_OBJECT_TYPE_DEFAULT
+ - Revision: DOT11_BSSID_LIST_REVISION_1
+ - Size: sizeof(DOT11_BSSID_LIST)
+ - uNumOfEntries
+ The number of entries in this structure.
+ - uTotalNumOfEntries
+ The total number of entries supported.
+ - BSSIDs
+ `https://msdn.microsoft.com/en-us/library/windows/desktop/
+ bb427397(v=vs.85).aspx`
+ A list of BSS identifiers.
+ - bssType
+ `https://msdn.microsoft.com/en-us/library/windows/desktop/
+ ms706001(v=vs.85).aspx`
+ Constants:
+ dot11_BSS_type_infrastructure = 1,
+ dot11_BSS_type_independent = 2,
+ dot11_BSS_type_any = 3
+ - flags
+ Constant: WLAN_CONNECTION_HIDDEN_NETWORK value: 0x00000001
+ Constant: WLAN_CONNECTION_ADHOC_JOIN_ONLY value: 0x00000002
+ Constant: WLAN_CONNECTION_IGNORE_PRIVACY_BIT value: 0x00000004
+ Constant: WLAN_CONNECTION_EAPOL_PASSTHROUGH value: 0x00000008
+ - password
+ '''
+ wifi_lib.connect(network, parameters)
+ return
+
+ def _disconnect(self):
+ '''
+ Disconnect from network.
+ '''
+ wifi_lib.disconnect()
+ return
+
+
+def instance():
+ return WindowWifi()
diff --git a/sbapp/plyer/tests/__init__.py b/sbapp/plyer/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/sbapp/plyer/tests/common.py b/sbapp/plyer/tests/common.py
new file mode 100644
index 00000000..7436db82
--- /dev/null
+++ b/sbapp/plyer/tests/common.py
@@ -0,0 +1,76 @@
+'''
+Common objects for testing
+==========================
+
+* :class:`PlatformTest` - used as a decorator, allows running a test function
+ only on a specific platform (see `plyer.utils.platform`).
+* :func:`platform_import` - manual import of a platform specific class instead
+ of using `plyer.facades.*` proxies.
+'''
+
+import traceback
+from os import sep
+from os.path import normpath, splitdrive
+from plyer.utils import platform as plyer_platform
+
+
+class PlatformTest:
+ '''
+ Class for the @PlatformTest decorator to prevent running tests
+ calling platform dependent API on different platforms.
+ '''
+
+ def __init__(self, platform):
+ self.platform = platform
+
+ def __call__(self, func):
+ platform = self.platform
+
+ if platform != plyer_platform:
+ print("Skipping test '{}' - not on '{}'".format(
+ func.__name__, platform
+ ))
+ func = self.eat
+ return func
+
+ @staticmethod
+ def eat(*args, **kwargs):
+ '''
+ Simply eat all positional and keyword arguments
+ and return None as an empty function.
+ '''
+
+
+def platform_import(platform, module_name, whereis_exe=None):
+ '''
+ Import platform API directly instead of through Proxy.
+ '''
+
+ try:
+ module = 'plyer.platforms.{}.{}'.format(
+ platform, module_name
+ )
+ mod = __import__(module, fromlist='.')
+
+ except ImportError as exc:
+ print(vars(exc))
+ traceback.print_exc()
+
+ if whereis_exe:
+ mod.whereis_exe = whereis_exe
+ return mod
+
+
+def splitpath(path):
+ '''
+ Split string path into a list of folders (+ file if available).
+ '''
+ if path[0] == sep and path[1] != sep:
+ path = path[1:]
+ path = normpath(path).split(sep)
+ else:
+ drive, path = splitdrive(path)
+ if path[0] == sep and path[1] != sep:
+ path = path[1:]
+ path = [drive, ] + normpath(path).split(sep)
+ return path
diff --git a/sbapp/plyer/tests/images/kivy32.ico b/sbapp/plyer/tests/images/kivy32.ico
new file mode 100644
index 00000000..2010bf3c
Binary files /dev/null and b/sbapp/plyer/tests/images/kivy32.ico differ
diff --git a/sbapp/plyer/tests/test_audio.py b/sbapp/plyer/tests/test_audio.py
new file mode 100644
index 00000000..e8118913
--- /dev/null
+++ b/sbapp/plyer/tests/test_audio.py
@@ -0,0 +1,110 @@
+'''
+TestAudio
+=========
+
+Tested platforms:
+
+* macOS
+* Windows
+
+.. versionadded:: 1.4.0
+'''
+
+import unittest
+import time
+
+from os import mkdir, remove, environ
+from os.path import join, expanduser, exists
+from plyer.tests.common import platform_import, PlatformTest
+
+
+class TestAudio(unittest.TestCase):
+ '''
+ TestCase for plyer.audio.
+
+ .. versionadded:: 1.4.0
+ '''
+
+ @PlatformTest('macosx')
+ def test_audio_macosx(self):
+ '''
+ Test macOS audio start, stop and play
+
+ .. versionadded:: 1.4.0
+ '''
+
+ path = join(expanduser('~'), 'Music')
+ if not exists(path):
+ mkdir(path)
+
+ audio = platform_import(
+ platform='macosx',
+ module_name='audio',
+ )
+
+ self.assertIn('OSXAudio', dir(audio))
+ audio = audio.instance()
+ self.assertIn('OSXAudio', str(audio))
+
+ self.assertFalse(exists(audio.file_path))
+ self.assertIsNone(audio.start())
+ time.sleep(0.5)
+ self.assertIsNone(audio.stop())
+ self.assertIsNone(audio.play())
+ time.sleep(0.5)
+ self.assertIsNone(audio.stop())
+
+ audio.file_path = audio.file_path.replace(
+ 'file://', ''
+ )
+
+ self.assertTrue(exists(audio.file_path))
+
+ remove(audio.file_path)
+
+ @PlatformTest('win')
+ def test_audio_win(self):
+ '''
+ Test Windows audio start, stop and play
+
+ .. versionadded:: 1.4.0
+ '''
+
+ if environ.get('APPVEYOR'):
+ # Appveyor has no recording device installed
+ # therefore the test will 100% fail
+ #
+ # error_code: 328
+ # message:
+ # 'No wave device is installed that can record files in the current
+ # format. To install a wave device, go to Control Panel, click P')
+ return
+
+ path = join(environ['USERPROFILE'], 'Music')
+ if not exists(path):
+ mkdir(path)
+
+ audio = platform_import(
+ platform='win',
+ module_name='audio',
+ )
+
+ self.assertIn('WinAudio', dir(audio))
+ audio = audio.instance()
+ self.assertIn('WinAudio', str(audio))
+
+ self.assertFalse(exists(audio.file_path))
+ self.assertIsNone(audio.start())
+ time.sleep(0.5)
+ self.assertIsNone(audio.stop())
+ self.assertIsNone(audio.play())
+ time.sleep(0.5)
+ self.assertIsNone(audio.stop())
+
+ self.assertTrue(exists(audio.file_path))
+
+ remove(audio.file_path)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/sbapp/plyer/tests/test_battery.py b/sbapp/plyer/tests/test_battery.py
new file mode 100644
index 00000000..b44d504d
--- /dev/null
+++ b/sbapp/plyer/tests/test_battery.py
@@ -0,0 +1,413 @@
+'''
+TestBattery
+===========
+
+Tested platforms:
+
+* Windows
+* Linux - upower, kernel sysclass
+* macOS - ioreg
+'''
+
+import unittest
+from io import BytesIO
+from os.path import join
+from textwrap import dedent
+from mock import patch, Mock
+
+from plyer.tests.common import PlatformTest, platform_import
+
+
+class MockedKernelSysclass:
+ '''
+ Mocked object used instead of Linux's sysclass for power_supply
+ battery uevent.
+ '''
+
+ @property
+ def path(self):
+ '''
+ Mocked path to Linux kernel sysclass.
+ '''
+ return join('/sys', 'class', 'power_supply', 'BAT0')
+
+ @property
+ def charging(self):
+ '''
+ Mocked battery charging status.
+ '''
+ return u'Discharging'
+
+ @property
+ def percentage(self):
+ '''
+ Mocked battery charge percentage.
+ '''
+ return 89.0
+
+ @property
+ def full(self):
+ '''
+ Mocked full battery charge.
+ '''
+ return 4764000
+
+ @property
+ def now(self):
+ '''
+ Calculated current mocked battery charge.
+ '''
+ return self.percentage * self.full / 100.0
+
+ @property
+ def uevent(self):
+ '''
+ Mocked /sys/class/power_supply/BAT0 file.
+ '''
+ return BytesIO(dedent(b'''\
+ POWER_SUPPLY_NAME=BAT0
+ POWER_SUPPLY_STATUS={}
+ POWER_SUPPLY_PRESENT=1
+ POWER_SUPPLY_TECHNOLOGY=Li-ion
+ POWER_SUPPLY_CYCLE_COUNT=0
+ POWER_SUPPLY_VOLTAGE_MIN_DESIGN=10800000
+ POWER_SUPPLY_VOLTAGE_NOW=12074000
+ POWER_SUPPLY_CURRENT_NOW=1584000
+ POWER_SUPPLY_CHARGE_FULL_DESIGN=5800000
+ POWER_SUPPLY_CHARGE_FULL={}
+ POWER_SUPPLY_CHARGE_NOW={}
+ POWER_SUPPLY_CAPACITY={}
+ POWER_SUPPLY_CAPACITY_LEVEL=Normal
+ POWER_SUPPLY_MODEL_NAME=1005HA
+ POWER_SUPPLY_MANUFACTURER=ASUS
+ POWER_SUPPLY_SERIAL_NUMBER=0
+ '''.decode('utf-8').format(
+ self.charging, self.full,
+ self.now, int(self.percentage)
+ )).encode('utf-8'))
+
+
+class MockedUPower:
+ '''
+ Mocked object used instead of 'upower' binary in the Linux specific API
+ plyer.platforms.linux.battery. The same output structure is tested for
+ the range of <min_version, max_version>.
+
+ .. note:: Extend the object with another data sample if it does not match.
+ '''
+
+ min_version = '0.99.4'
+ max_version = '0.99.4'
+
+ values = {
+ u'Device': u'/org/freedesktop/UPower/devices/battery_BAT0',
+ u'native-path': u'BAT0',
+ u'vendor': u'ASUS',
+ u'model': u'1005HA',
+ u'power supply': u'yes',
+ u'updated': u'Thu 05 Jul 2018 23:15:01 PM CEST',
+ u'has history': u'yes',
+ u'has statistics': u'yes',
+ u'battery': {
+ u'present': u'yes',
+ u'rechargeable': u'yes',
+ u'state': u'discharging',
+ u'warning-level': u'none',
+ u'energy': u'48,708 Wh',
+ u'energy-empty': u'0 Wh',
+ u'energy-full': u'54,216 Wh',
+ u'energy-full-design': u'62,64 Wh',
+ u'energy-rate': u'7,722 W',
+ u'voltage': u'11,916 V',
+ u'time to empty': u'6,3 hours',
+ u'percentage': u'89%',
+ u'capacity': u'86,5517%',
+ u'technology': u'lithium-ion',
+ u'icon-name': u"'battery-full-symbolic"
+ },
+ u'History (charge)': u'1530959637 89,000 discharging',
+ u'History (rate)': u'1530958556 7,474 discharging'
+ }
+
+ data = str(
+ ' native-path: {native-path}\n'
+ ' vendor: {vendor}\n'
+ ' model: {model}\n'
+ ' power supply: {power supply}\n'
+ ' updated: {updated}\n'
+ ' has history: {has history}\n'
+ ' has statistics: {has statistics}\n'
+ ' battery\n'
+ ' present: {battery[present]}\n'
+ ' rechargeable: {battery[rechargeable]}\n'
+ ' state: {battery[state]}\n'
+ ' warning-level: {battery[warning-level]}\n'
+ ' energy: {battery[energy]}\n'
+ ' energy-empty: {battery[energy-empty]}\n'
+ ' energy-full: {battery[energy-full]}\n'
+ ' energy-full-design: {battery[energy-full-design]}\n'
+ ' energy-rate: {battery[energy-rate]}\n'
+ ' voltage: {battery[voltage]}\n'
+ ' time to empty: {battery[time to empty]}\n'
+ ' percentage: {battery[percentage]}\n'
+ ' capacity: {battery[capacity]}\n'
+ ' technology: {battery[technology]}\n'
+ ' icon-name: {battery[icon-name]}\n'
+ ' History (charge):\n'
+ ' {History (charge)}\n'
+ ' History (rate):\n'
+ ' {History (rate)}\n'
+ ).format(**values).encode('utf-8')
+ # LinuxBattery calls decode()
+
+ def __init__(self, *args, **kwargs):
+ # only to ignore all args, kwargs
+ pass
+
+ @staticmethod
+ def communicate():
+ '''
+ Mock Popen.communicate, so that 'upower' isn't used.
+ '''
+ return (MockedUPower.data, )
+
+ @staticmethod
+ def whereis_exe(binary):
+ '''
+ Mock whereis_exe, so that it looks like
+ Linux UPower binary is present on the system.
+ '''
+ return binary == 'upower'
+
+ @staticmethod
+ def charging():
+ '''
+ Return charging bool from mocked data.
+ '''
+ return MockedUPower.values['battery']['state'] == 'charging'
+
+ @staticmethod
+ def percentage():
+ '''
+ Return percentage from mocked data.
+ '''
+ percentage = MockedUPower.values['battery']['percentage'][:-1]
+ return float(percentage.replace(',', '.'))
+
+
+class MockedIOReg:
+ '''
+ Mocked object used instead of Apple's ioreg.
+ '''
+ values = {
+ "MaxCapacity": "5023",
+ "CurrentCapacity": "4222",
+ "IsCharging": "No"
+ }
+
+ output = dedent(
+ """+-o AppleSmartBattery <class AppleSmartBattery,\
+ id 0x1000002c9, registered, matched, active, busy 0 (0 ms), retain 6>
+ {{
+ "TimeRemaining" = 585
+ "AvgTimeToEmpty" = 585
+ "InstantTimeToEmpty" = 761
+ "ExternalChargeCapable" = Yes
+ "FullPathUpdated" = 1541845134
+ "CellVoltage" = (4109,4118,4099,0)
+ "PermanentFailureStatus" = 0
+ "BatteryInvalidWakeSeconds" = 30
+ "AdapterInfo" = 0
+ "MaxCapacity" = {MaxCapacity}
+ "Voltage" = 12326
+ "DesignCycleCount70" = 13
+ "Manufacturer" = "SWD"
+ "Location" = 0
+ "CurrentCapacity" = {CurrentCapacity}
+ "LegacyBatteryInfo" = {{"Amperage"=18446744073709551183,"Flags"=4,\
+ "Capacity"=5023,"Current"=4222,"Voltage"=12326,"Cycle Count"=40}}
+ "FirmwareSerialNumber" = 1
+ "BatteryInstalled" = Yes
+ "PackReserve" = 117
+ "CycleCount" = 40
+ "DesignCapacity" = 5088
+ "OperationStatus" = 58435
+ "ManufactureDate" = 19700
+ "AvgTimeToFull" = 65535
+ "BatterySerialNumber" = "1234567890ABCDEFGH"
+ "BootPathUpdated" = 1541839734
+ "PostDischargeWaitSeconds" = 120
+ "Temperature" = 3038
+ "UserVisiblePathUpdated" = 1541845194
+ "InstantAmperage" = 18446744073709551249
+ "ManufacturerData" = <000000000>
+ "FullyCharged" = No
+ "MaxErr" = 1
+ "DeviceName" = "bq20z451"
+ "IOGeneralInterest" = "IOCommand is not serializable"
+ "Amperage" = 18446744073709551183
+ "IsCharging" = {IsCharging}
+ "DesignCycleCount9C" = 1000
+ "PostChargeWaitSeconds" = 120
+ "ExternalConnected" = No
+ }}"""
+ ).format(**values).encode('utf-8')
+
+ def __init__(self, *args, **kwargs):
+ # only to ignore all args, kwargs
+ pass
+
+ @staticmethod
+ def communicate():
+ '''
+ Mock Popen.communicate, so that 'ioreg' isn't used.
+ '''
+ return (MockedIOReg.output, )
+
+ @staticmethod
+ def whereis_exe(binary):
+ '''
+ Mock whereis_exe, so that it looks like
+ macOS ioreg binary is present on the system.
+ '''
+ return binary == 'ioreg'
+
+ @staticmethod
+ def charging():
+ '''
+ Return charging bool from mocked data.
+ '''
+ return MockedIOReg.values['IsCharging'] == 'Yes'
+
+ @staticmethod
+ def percentage():
+ '''
+ Return percentage from mocked data.
+ '''
+ current_capacity = int(MockedIOReg.values['CurrentCapacity'])
+ max_capacity = int(MockedIOReg.values['MaxCapacity'])
+ percentage = 100.0 * current_capacity / max_capacity
+
+ return percentage
+
+
+class TestBattery(unittest.TestCase):
+ '''
+ TestCase for plyer.battery.
+ '''
+
+ def test_battery_linux_upower(self):
+ '''
+ Test mocked Linux UPower for plyer.battery.
+ '''
+ battery = platform_import(
+ platform='linux',
+ module_name='battery',
+ whereis_exe=MockedUPower.whereis_exe
+ )
+ battery.Popen = MockedUPower
+ battery = battery.instance()
+
+ self.assertEqual(
+ battery.status, {
+ 'isCharging': MockedUPower.charging(),
+ 'percentage': MockedUPower.percentage()
+ }
+ )
+
+ def test_battery_linux_kernel(self):
+ '''
+ Test mocked Linux kernel sysclass for plyer.battery.
+ '''
+
+ def false(*args, **kwargs):
+ return False
+
+ sysclass = MockedKernelSysclass()
+
+ with patch(target='os.path.exists') as bat_path:
+ # first call to trigger exists() call
+ platform_import(
+ platform='linux',
+ module_name='battery',
+ whereis_exe=false
+ ).instance()
+ bat_path.assert_called_once_with(sysclass.path)
+
+ # exists() checked with sysclass path
+ # set mock to proceed with this branch
+ bat_path.return_value = True
+
+ battery = platform_import(
+ platform='linux',
+ module_name='battery',
+ whereis_exe=false
+ ).instance()
+
+ stub = Mock(return_value=sysclass.uevent)
+ target = 'builtins.open'
+
+ with patch(target=target, new=stub):
+ self.assertEqual(
+ battery.status, {
+ 'isCharging': sysclass.charging == 'Charging',
+ 'percentage': sysclass.percentage
+ }
+ )
+
+ @PlatformTest('win')
+ def test_battery_win(self):
+ '''
+ Test Windows API for plyer.battery.
+ '''
+ battery = platform_import(
+ platform='win',
+ module_name='battery'
+ ).instance()
+ for key in ('isCharging', 'percentage'):
+ self.assertIn(key, battery.status)
+ self.assertIsNotNone(battery.status[key])
+
+ def test_battery_macosx(self):
+ '''
+ Test macOS IOReg for plyer.battery.
+ '''
+ battery = platform_import(
+ platform='macosx',
+ module_name='battery',
+ whereis_exe=MockedIOReg.whereis_exe
+ )
+
+ battery.Popen = MockedIOReg
+ self.assertIn('OSXBattery', dir(battery))
+ battery = battery.instance()
+ self.assertIn('OSXBattery', str(battery))
+
+ self.assertEqual(
+ battery.status, {
+ 'isCharging': MockedIOReg.charging(),
+ 'percentage': MockedIOReg.percentage()
+ }
+ )
+
+ def test_battery_macosx_instance(self):
+ '''
+ Test macOS instance for plyer.battery
+ '''
+
+ def no_exe(*args, **kwargs):
+ return
+
+ battery = platform_import(
+ platform='macosx',
+ module_name='battery',
+ whereis_exe=no_exe
+ )
+
+ battery = battery.instance()
+ self.assertNotIn('OSXBattery', str(battery))
+ self.assertIn('Battery', str(battery))
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/sbapp/plyer/tests/test_bluetooth.py b/sbapp/plyer/tests/test_bluetooth.py
new file mode 100644
index 00000000..8997c4d3
--- /dev/null
+++ b/sbapp/plyer/tests/test_bluetooth.py
@@ -0,0 +1,144 @@
+'''
+TestBluetooth
+=============
+
+Tested platforms:
+
+* macOS - system_profiler
+'''
+
+import unittest
+
+from plyer.tests.common import platform_import
+from textwrap import dedent
+
+
+class MockedSystemProfiler:
+ '''
+ Mocked object used instead of Apple's system_profiler
+ '''
+ value = "On"
+ output = dedent(
+ """Bluetooth:
+
+ Apple Bluetooth Software Version: 6.0.7f11
+ Hardware, Features, and Settings:
+ Address: AA-00-BB-11-CC-22
+ Bluetooth Low Energy Supported: Yes
+ Handoff Supported: Yes
+ Instant Hot Spot Supported: Yes
+ Manufacturer: Broadcom
+ Transport: UART
+ Chipset: 1234
+ Firmware Version: v00 c0000
+ Bluetooth Power: {}
+ Auto Seek Pointing: On
+ Remote wake: On
+ Vendor ID: 0x0000
+ Product ID: 0x0000
+ HCI Version: (0x0)
+ HCI Revision: 0x0000
+ LMP Version: (0x0)
+ LMP Subversion: 0x0000
+ Auto Seek Keyboard: On
+ Devices (Paired, Configured, etc.):
+ iPhone:
+ Address: AA-00-BB-11-CC-22
+ Major Type: Miscellaneous
+ Minor Type: Unknown
+ Services:
+ Paired: No
+ Configured: Yes
+ Connected: No
+ Class of Device: 0x00 0x00 0x0000
+ Services:
+ Bluetooth File Transfer:
+ Folder other devices can browse: ~/Public
+ When receiving items: Accept all without warning
+ State: Disabled
+ Bluetooth File Exchange:
+ Folder for accepted items: ~/Downloads
+ When other items are accepted: Save to location
+ When receiving items: Accept all without warning
+ State: Disabled
+ Bluetooth Internet Sharing:
+ State: Disabled
+ Incoming Serial Ports:
+ Bluetooth-Incoming-Port:
+ RFCOMM Channel: 3
+ Requires Authentication: No"""
+ ).format(value).encode('utf-8')
+
+ def __init__(self, *args, **kwargs):
+ # only to ignore all args, kwargs
+ pass
+
+ @staticmethod
+ def communicate():
+ '''
+ Mock Popen.communicate, so that 'system_profiler'
+ isn't used.
+ '''
+ return (MockedSystemProfiler.output, )
+
+ @staticmethod
+ def whereis_exe(binary):
+ '''
+ Mock whereis_exe, so that it looks like
+ macOS system_profiler binary is present on the system.
+ '''
+ return binary == 'system_profiler'
+
+ @staticmethod
+ def get_info():
+ '''
+ Return current bluetooth status from mocked output.
+ '''
+ return MockedSystemProfiler.value
+
+
+class TestBluetooth(unittest.TestCase):
+ '''
+ TestCase for plyer.bluetooth.
+ '''
+
+ def test_bluetooth_macosx(self):
+ '''
+ Test macOS system_profiler for plyer.bluetooth.
+ '''
+ bluetooth = platform_import(
+ platform='macosx',
+ module_name='bluetooth',
+ whereis_exe=MockedSystemProfiler.whereis_exe
+ )
+
+ bluetooth.Popen = MockedSystemProfiler
+ self.assertIn('OSXBluetooth', dir(bluetooth))
+ bluetooth = bluetooth.instance()
+ self.assertIn('OSXBluetooth', str(bluetooth))
+
+ self.assertEqual(
+ bluetooth.info, MockedSystemProfiler.get_info()
+ )
+
+ def test_bluetooth_macosx_instance(self):
+ '''
+ Test macOS instance for plyer.bluetooth.
+ '''
+
+ def no_exe(*args, **kwargs):
+ return
+
+ bluetooth = platform_import(
+ platform='macosx',
+ module_name='bluetooth',
+ whereis_exe=no_exe
+ )
+
+ bluetooth = bluetooth.instance()
+ self.assertNotIn('OSXBluetooth', str(bluetooth))
+ self.assertIn('Bluetooth', str(bluetooth))
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/sbapp/plyer/tests/test_cpu.py b/sbapp/plyer/tests/test_cpu.py
new file mode 100644
index 00000000..8882522c
--- /dev/null
+++ b/sbapp/plyer/tests/test_cpu.py
@@ -0,0 +1,262 @@
+'''
+TestCPU
+=======
+
+Tested platforms:
+
+* Windows
+* Linux - nproc
+'''
+
+import unittest
+from os import environ
+from os.path import join
+from mock import patch, Mock
+from textwrap import dedent
+
+from plyer.tests.common import PlatformTest, platform_import, splitpath
+
+
+class MockedKernelCPU:
+ def __init__(self, *args, **kwargs):
+ self.fname = args[0] if args else ''
+ self.cpu_path = join('/sys', 'devices', 'system', 'cpu')
+ self.cores = 16
+ self.indicies = 4
+
+ def __enter__(self, *args):
+ file_value = None
+ cpu_path = self.cpu_path
+ spath = splitpath(self.fname)
+
+ if self.fname == join(cpu_path, 'present'):
+ file_value = Mock()
+ file_value.read.return_value = self.present
+ elif spath[5] == 'cache' and spath[7] == 'level':
+ file_value = Mock()
+ # force bytes, because reading files as bytes
+ file_value.read.return_value = str(
+ self.index_types[spath[4]][spath[6]][spath[7]]
+ ).encode('utf-8')
+ return file_value
+
+ def __exit__(self, *args):
+ pass
+
+ @property
+ def present(self):
+ rng = list(range(self.cores))
+ start = str(rng[0])
+ end = str(rng[-1])
+ if start == end: # cores == 1 --> b'0'
+ value = str(start)
+ else: # cores > 1 --> b'0-n'
+ value = str('-'.join([start, end]))
+ return value.encode('utf-8')
+
+ @property
+ def listdir(self):
+ return ['index{}'.format(i) for i in range(self.indicies)]
+
+ @property
+ def index_types(self):
+ # assign L1 to index0-1, L2 to 2, L3 to 3
+ types = {0: 1, 1: 1, 2: 2, 3: 3}
+
+ return {
+ 'cpu{}'.format(c): {
+ 'index{}'.format(i): {
+ 'level': types[i]
+ }
+ for i in range(self.indicies)
+ }
+ for c in range(self.cores)
+ }
+
+
+class MockedNProc:
+ '''
+ Mocked object used instead of 'nproc' binary in the Linux specific API
+ plyer.platforms.linux.cpu. The same output structure is tested for
+ the range of <min_version, max_version>.
+
+ .. note:: Extend the object with another data sample if it does not match.
+ '''
+
+ min_version = '8.21'
+ max_version = '8.21'
+ logical_cores = 99
+
+ def __init__(self, *args, **kwargs):
+ # only to ignore all args, kwargs
+ pass
+
+ @staticmethod
+ def communicate():
+ '''
+ Mock Popen.communicate, so that 'nproc' isn't used.
+ '''
+ return (str(MockedNProc.logical_cores).encode('utf-8'), )
+
+ @staticmethod
+ def whereis_exe(binary):
+ '''
+ Mock whereis_exe, so that it looks like
+ Linux NProc binary is present on the system.
+ '''
+ return binary == 'nproc'
+
+ @staticmethod
+ def logical():
+ '''
+ Return percentage from mocked data.
+ '''
+ return int(MockedNProc.logical_cores)
+
+
+class MockedProcinfo:
+ # docs:
+ # https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
+ # /tree/arch/x86/kernel/cpu/proc.c
+ sockets = 1 # physical id
+ physical = 2 # core id
+ threads_per_core = 2 # Intel specs document for i7-4500U
+ logical = physical * threads_per_core # processor
+
+ def __init__(self, *args, **kwargs):
+ self.fname = args[0] if args else ''
+
+ self.output = []
+ __step = 0 # 0,1,0,1 -> 0,0,1,1
+ for soc in range(self.sockets):
+ for log in range(self.logical):
+ if log != 0 and not log % self.physical:
+ __step += 1
+ self.output.append((dedent(
+ '''\
+ processor\t: {logical}
+ vendor_id\t: GenuineIntel
+ cpu family\t: 6
+ model\t\t: 69
+ model name\t: Intel(R) Core(TM) i7-4500U CPU @ 1.80GHz
+ stepping\t: 1
+ microcode\t: 0x17
+ cpu MHz\t\t: 774.000
+ cache size\t: 4096 KB
+ physical id\t: {socket}
+ siblings\t: 4
+ core id\t\t: {physical}
+ cpu cores\t: {threads_per_core}
+ apicid\t\t: {logical}
+ initial apicid\t: 0
+ fpu\t\t: yes
+ fpu_exception\t: yes
+ cpuid level\t: 13
+ wp\t\t: yes
+ flags\t\t: fpu vme de pse tsc msr pae mce cx8 ...
+ bogomips\t: 3591.40
+ clflush size\t: 64
+ cache_alignment\t: 64
+ address sizes\t: 39 bits physical, 48 bits virtual
+ power management:
+ \n'''
+ )).format(**{
+ 'socket': soc,
+ 'physical': __step,
+ 'logical': log,
+ 'threads_per_core': self.threads_per_core
+ }))
+ self.output = ''.join(self.output).encode('utf-8')
+
+ def __enter__(self, *args):
+ file_value = None
+
+ if self.fname == '/proc/cpuinfo':
+ file_value = Mock()
+ file_value.readlines.return_value = self.output.split(
+ '\n'.encode('utf-8')
+ )
+ return file_value
+
+ def __exit__(self, *args):
+ pass
+
+
+class TestCPU(unittest.TestCase):
+ '''
+ TestCase for plyer.cpu.
+ '''
+
+ def test_cpu_linux_physical(self):
+ cpu = platform_import(
+ platform='linux',
+ module_name='cpu',
+ whereis_exe=lambda b: b == 'nproc'
+ ).instance()
+
+ stub = MockedProcinfo
+ target = 'builtins.open'
+
+ with patch(target=target, new=stub):
+ sb = stub()
+ self.assertEqual(
+ cpu.physical, sb.physical
+ )
+
+ def test_cpu_linux_logical(self):
+ '''
+ Test mocked Linux NProc for plyer.cpu.
+ '''
+ cpu = platform_import(
+ platform='linux',
+ module_name='cpu',
+ whereis_exe=MockedNProc.whereis_exe
+ )
+ cpu.Popen = MockedNProc
+ cpu = cpu.instance()
+
+ self.assertEqual(
+ cpu.logical, MockedNProc.logical()
+ )
+
+ @PlatformTest('linux')
+ def test_cpu_linux_cache(self):
+ cpu = platform_import(
+ platform='linux',
+ module_name='cpu',
+ whereis_exe=lambda b: b == 'nproc'
+ ).instance()
+
+ stub = MockedKernelCPU
+ target = 'builtins.open'
+ sub_target = 'plyer.platforms.linux.cpu.listdir'
+
+ with patch(target=target, new=stub):
+ with patch(target=sub_target, return_value=stub().listdir):
+ sb = stub()
+ self.assertEqual(
+ cpu.cache, {
+ 'L1': sb.cores * 2,
+ 'L2': sb.cores,
+ 'L3': sb.cores
+ }
+ )
+
+ @PlatformTest('win')
+ def test_cpu_win_logical(self):
+ cpu = platform_import(
+ platform='win',
+ module_name='cpu'
+ )
+
+ cpu = cpu.instance()
+ self.assertEqual(
+ cpu.logical,
+ # https://docs.microsoft.com/en-us/previous-versions/
+ # windows/it-pro/windows-xp/bb490954(v=technet.10)
+ int(environ['NUMBER_OF_PROCESSORS'])
+ )
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/sbapp/plyer/tests/test_devicename.py b/sbapp/plyer/tests/test_devicename.py
new file mode 100644
index 00000000..cbb0fc4b
--- /dev/null
+++ b/sbapp/plyer/tests/test_devicename.py
@@ -0,0 +1,77 @@
+'''
+TestDeviceName
+============
+
+Tested platforms:
+
+* Windows
+'''
+
+import unittest
+from mock import patch
+from plyer.tests.common import PlatformTest, platform_import
+import socket
+
+
+class TestDeviceName(unittest.TestCase):
+ '''
+ TestCase for plyer.devicename.
+ '''
+
+ @PlatformTest('win')
+ def test_devicename_win(self):
+ '''
+ Test Windows API for plyer.devicename.
+ '''
+ devicename = platform_import(platform='win',
+ module_name='devicename'
+ )
+ devicename_instance = devicename.instance()
+
+ with patch.object(socket,
+ 'gethostname',
+ return_value='mocked_windows_hostname'
+ ) as _:
+
+ evaluated_device_name = devicename_instance.device_name
+ self.assertEqual(evaluated_device_name, 'mocked_windows_hostname')
+
+ @PlatformTest('linux')
+ def test_devicename_linux(self):
+ '''
+ Test Linux API for plyer.devicename.
+ '''
+ devicename = platform_import(platform='linux',
+ module_name='devicename'
+ )
+ devicename_instance = devicename.instance()
+
+ with patch.object(socket,
+ 'gethostname',
+ return_value='mocked_linux_hostname'
+ ) as _:
+
+ evaluated_device_name = devicename_instance.device_name
+ self.assertEqual(evaluated_device_name, 'mocked_linux_hostname')
+
+ @PlatformTest('macosx')
+ def test_devicename_macosx(self):
+ '''
+ Test MacOSX API for plyer.devicename.
+ '''
+ devicename = platform_import(platform='macosx',
+ module_name='devicename'
+ )
+ devicename_instance = devicename.instance()
+
+ with patch.object(socket,
+ 'gethostname',
+ return_value='mocked_macosx_hostname'
+ ) as _:
+
+ evaluated_device_name = devicename_instance.device_name
+ self.assertEqual(evaluated_device_name, 'mocked_macosx_hostname')
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/sbapp/plyer/tests/test_email.py b/sbapp/plyer/tests/test_email.py
new file mode 100644
index 00000000..60aa86f0
--- /dev/null
+++ b/sbapp/plyer/tests/test_email.py
@@ -0,0 +1,48 @@
+'''
+TestEmail
+=========
+
+Tested platforms:
+
+* Windows
+'''
+
+import unittest
+
+from mock import Mock, patch
+from plyer.tests.common import PlatformTest, platform_import
+
+
+class TestEmail(unittest.TestCase):
+ '''
+ TestCase for plyer.email.
+ '''
+
+ @staticmethod
+ @PlatformTest('win')
+ def test_email_win():
+ '''
+ Test starting Windows email client for plyer.email.
+ '''
+ email = platform_import(
+ platform='win',
+ module_name='email'
+ )
+
+ try:
+ test_mailto = 'mailto:recipient?subject=subject&body=text'
+ with patch(target='os.startfile', new=Mock()) as startfile:
+ email.instance().send(
+ recipient='recipient',
+ subject='subject',
+ text='text'
+ )
+ startfile.assert_called_once_with(test_mailto)
+ except WindowsError:
+ # if WE is raised, email client isn't found,
+ # but the platform code works correctly
+ print('Mail client not found!')
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/sbapp/plyer/tests/test_facade.py b/sbapp/plyer/tests/test_facade.py
new file mode 100644
index 00000000..b49b15f7
--- /dev/null
+++ b/sbapp/plyer/tests/test_facade.py
@@ -0,0 +1,183 @@
+'''
+TestFacade
+==========
+
+Tested platforms:
+
+* Android
+* iOS
+* Windows
+* MacOS
+* Linux
+'''
+
+import unittest
+
+import sys
+from types import MethodType
+
+from mock import Mock, patch
+
+import plyer
+
+
+def mock_platform_module(mod, platform, cls):
+ '''
+ Create a stub module for a specific platform. This module contains:
+
+ * class inheriting from facade implementing the desired feature
+ * 'instance' function returning an instance of the implementing class
+ '''
+
+ # assemble an instance returned from the instance() function
+ # which is created from a dynamically created class
+ # <class '<mod>.<platform><cls>'> e.g.:
+ # <class 'plyer.platforms.win.dummy . WinDummy'>
+ stub_inst = Mock(
+ __module__=mod,
+ __class__=type(
+ '{}{}'.format(platform.title(), cls), (object, ), {
+ '__module__': mod
+ }
+ ),
+ )
+
+ # manual 'return_value' assign to Mock, so that the instance() call
+ # can return stub_inst's own instance instead of creating another
+ # unnecessary Mock object
+ stub_inst.return_value = stub_inst
+
+ # bind custom function returning the class name to stub_inst instance,
+ # so that instance().show() call requires 'self' i.e. instance parameter
+ # for the function to access the instance's class name
+ stub_inst.show = MethodType(lambda slf: slf, stub_inst)
+
+ stub_mod = Mock(instance=stub_inst)
+ return stub_mod
+
+
+# dummy pyjnius class to silence the import + config
+class DummyJnius:
+ '''
+ Mocked PyJNIus module.
+ '''
+
+ def __init__(self, *args, **kwargs):
+ class JavaClass:
+ '''
+ Mocked PyJNIus JavaClass object.
+ '''
+
+ def __init__(self):
+ self.ANDROID_VERSION = None
+ self.SDK_INT = 1
+ self.mActivity = None
+
+ self.autoclass = lambda *a, **kw: JavaClass()
+
+
+class TestFacade(unittest.TestCase):
+ '''
+ TestCase for plyer.utils.Proxy and plyer.facades.
+ '''
+
+ def test_facade_existing_platforms(self):
+ '''
+ Test for returning an object for Android API implementation
+ from Proxy object using a dynamically generated dummy objects.
+ '''
+ _original = plyer.utils.platform
+
+ for plat in {'android', 'ios', 'win', 'macosx', 'linux'}:
+ plyer.utils.platform = plat
+
+ if plat == 'android':
+ # android platform automatically imports jnius
+ sys.modules['jnius'] = DummyJnius()
+
+ # create stub module with instance func and class
+ stub_mod = mock_platform_module(
+ mod='plyer.platforms.{}.dummy'.format(plat),
+ platform=plyer.utils.platform,
+ cls='Dummy'
+ )
+
+ proxy_cls = plyer.utils.Proxy
+ target = 'builtins.__import__'
+
+ with patch(target=target, return_value=stub_mod):
+ dummy = proxy_cls('dummy', stub_mod)
+
+ self.assertEqual(
+ str(dummy.__class__).split("'")[1],
+ 'plyer.platforms.{}.dummy.{}Dummy'.format(
+ plat, plat.title()
+ )
+ )
+ self.assertEqual(
+ str(dummy.show().__class__).split("'")[1],
+ 'plyer.platforms.{}.dummy.{}Dummy'.format(
+ plat, plat.title()
+ )
+ )
+
+ plyer.utils.platform = _original
+
+ def test_facade_unknown(self):
+ '''
+ Test fallback of Proxy to facade if there
+ is no such requested platform.
+ '''
+
+ _original = plyer.utils.platform
+ plyer.utils.platform = 'unknown'
+
+ # no 'unknown' platform (folder), fallback to facade
+ class MockedProxy(plyer.utils.Proxy):
+ '''
+ Partially mocked Proxy class, so that we pull the error
+ from traceback.print_exc to the test and check the calls.
+ '''
+
+ # _ensure_obj is called only once, to either
+ # get the platform object or fall back to facade
+ # therefore the three self.asserts below will return
+ # different values
+ expected_asserts = [True, False, False]
+
+ def _ensure_obj(inst):
+ # called once, prints to stderr
+
+ # mock stderr because traceback.print_exc uses it
+ # https://github.com/python/cpython/blob/
+ # 16dfca4d829e45f36e71bf43f83226659ce49315/Lib/traceback.py#L99
+ sys.stderr = Mock()
+
+ # call the original function to trigger
+ # ImportError warnings in stderr
+ super(MockedProxy, inst)._ensure_obj()
+
+ # Traceback (most recent call last):
+ # File "/app/plyer/utils.py", line 88, in _ensure_obj
+ # mod = __import__(module, fromlist='.')
+ # ImportError: No module named unknown.dummy
+
+ # must not use self.assertX
+ # (has to be checked on the go!)
+ expected_bool = MockedProxy.expected_asserts.pop(0)
+ call_count = sys.stderr.write.call_count
+ assert (call_count == 6) == expected_bool, call_count
+
+ # return stderr to the original state
+ sys.stderr = sys.__stderr__
+
+ proxy_cls = MockedProxy
+ facade = Mock()
+ dummy = proxy_cls('dummy', facade)
+
+ self.assertEqual(dummy._mock_new_parent, facade)
+ plyer.utils.platform = _original
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/sbapp/plyer/tests/test_notification.py b/sbapp/plyer/tests/test_notification.py
new file mode 100644
index 00000000..7177ad7a
--- /dev/null
+++ b/sbapp/plyer/tests/test_notification.py
@@ -0,0 +1,209 @@
+'''
+TestNotification
+================
+
+Tested platforms:
+
+* Windows
+* Linux - notify-send, dbus
+'''
+
+import unittest
+import sys
+
+from time import sleep
+from os.path import dirname, abspath, join
+
+from mock import Mock, patch
+from plyer.tests.common import PlatformTest, platform_import
+
+
+class MockedNotifySend:
+ '''
+ Mocked object used instead of the console-like calling
+ of notify-send binary with parameters.
+ '''
+ @staticmethod
+ def whereis_exe(binary):
+ '''
+ Mock whereis_exe, so that it looks like
+ Linux notify-send binary is present on the system.
+ '''
+ return binary == 'notify-send'
+
+ @staticmethod
+ def call(args):
+ '''
+ Mocked subprocess.call to check console parameters.
+ '''
+ assert len(args) >= 3
+ assert TestNotification.data['title'] in args
+ assert TestNotification.data['message'] in args
+
+ @staticmethod
+ def warn(msg):
+ '''
+ Mocked warnings.warn, so that we check the custom ImportError message.
+ '''
+ assert 'dbus package is not installed' in msg
+
+
+class TestNotification(unittest.TestCase):
+ '''
+ TestCase for plyer.notification.
+ '''
+
+ data = {
+ 'title': 'title',
+ 'message': 'My Message\nis multiline',
+ 'app_name': 'Plyer Test',
+ 'app_icon': join(
+ dirname(abspath(__file__)),
+ 'images', 'kivy32.ico'
+ ),
+ 'timeout': 0.7
+ }
+
+ def show_notification(self, instance):
+ '''
+ Call notify() from platform specific instance with sample data.
+ '''
+ instance.notify(**self.data)
+
+ @PlatformTest('win')
+ def test_notification_windows(self):
+ '''
+ Test Windows API for plyer.notification.
+ '''
+ import ctypes
+ from ctypes import (
+ WINFUNCTYPE, POINTER,
+ create_unicode_buffer,
+ c_bool, c_int
+ )
+ notif = platform_import(
+ platform='win',
+ module_name='notification'
+ ).instance()
+ enum_windows = ctypes.windll.user32.EnumWindows
+ get_class_name = ctypes.windll.user32.GetClassNameW
+
+ # loop over windows and get refs to
+ # the opened plyer notifications
+ clsnames = []
+
+ def fetch_class(hwnd, *args):
+ '''
+ EnumWindowsProc callback for EnumWindows.
+ '''
+ buff = create_unicode_buffer(50)
+ get_class_name(hwnd, buff, 50)
+
+ if 'Plyer' in buff.value:
+ clsnames.append(buff.value)
+
+ # ensure it's not an empty facade
+ self.assertIn('WindowsNotification', str(notif))
+
+ # create enum function for EnumWindows
+ enum_windows_proc = WINFUNCTYPE(
+ # returns
+ c_bool,
+
+ # input params: hwnd, lParam
+ POINTER(c_int), POINTER(c_int)
+ )
+
+ for i in range(3):
+ self.show_notification(notif)
+
+ # the balloon needs some time to became visible in WinAPI
+ sleep(0.2)
+
+ # fetch window class names
+ enum_windows(
+ # enum & params
+ enum_windows_proc(fetch_class), None
+ )
+
+ # 3 active balloons at the same time,
+ # class_name is incremented - see WindowsBalloonTip
+ self.assertEqual(len(clsnames), i + 1)
+ self.assertIn('PlyerTaskbar' + str(i), clsnames)
+ clsnames = []
+
+ @PlatformTest('linux')
+ def test_notification_dbus(self):
+ '''
+ Test mocked Linux DBus for plyer.notification.
+ '''
+ notif = platform_import(
+ platform='linux',
+ module_name='notification'
+ )
+ self.assertIn('NotifyDbus', dir(notif))
+
+ # (3) mocked Interface called from dbus
+ interface = Mock()
+ interface.side_effect = (interface, )
+
+ # (2) mocked SessionBus called from dbus
+ session_bus = Mock()
+ session_bus.side_effect = (session_bus, )
+
+ # (1) mocked dbus for import
+ dbus = Mock(SessionBus=session_bus, Interface=interface)
+
+ # inject the mocked module
+ self.assertNotIn('dbus', sys.modules)
+ sys.modules['dbus'] = dbus
+
+ try:
+ notif = notif.instance()
+ self.assertIn('NotifyDbus', str(notif))
+
+ # call notify()
+ self.show_notification(notif)
+
+ # check whether Mocks were called
+ dbus.SessionBus.assert_called_once()
+
+ session_bus.get_object.assert_called_once_with(
+ 'org.freedesktop.Notifications',
+ '/org/freedesktop/Notifications'
+ )
+
+ interface.Notify.assert_called_once_with(
+ TestNotification.data['app_name'],
+ 0,
+ TestNotification.data['app_icon'],
+ TestNotification.data['title'],
+ TestNotification.data['message'],
+ [], {},
+ TestNotification.data['timeout'] * 1000
+ )
+ finally:
+ del sys.modules['dbus']
+ self.assertNotIn('dbus', sys.modules)
+
+ @PlatformTest('linux')
+ def test_notification_notifysend(self):
+ '''
+ Test mocked Linux notify-send for plyer.notification.
+ '''
+ notif = platform_import(
+ platform='linux',
+ module_name='notification',
+ whereis_exe=MockedNotifySend.whereis_exe
+ )
+ self.assertIn('NotifySendNotification', dir(notif))
+ with patch(target='warnings.warn', new=MockedNotifySend.warn):
+ notif = notif.instance()
+ self.assertIn('NotifySendNotification', str(notif))
+
+ with patch(target='subprocess.call', new=MockedNotifySend.call):
+ self.assertIsNone(self.show_notification(notif))
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/sbapp/plyer/tests/test_screenshot.py b/sbapp/plyer/tests/test_screenshot.py
new file mode 100644
index 00000000..931fac28
--- /dev/null
+++ b/sbapp/plyer/tests/test_screenshot.py
@@ -0,0 +1,137 @@
+'''
+TestScreenshot
+==============
+
+Tested platforms:
+
+* MacOS
+* Linux
+'''
+
+import unittest
+
+from os import mkdir, remove
+from os.path import join, expanduser, exists
+
+from mock import patch
+from plyer.tests.common import PlatformTest, platform_import
+
+
+class MockedScreenCapture:
+ '''
+ Mocked object used instead of the console-like calling
+ of screencapture binary with parameters.
+ '''
+ @staticmethod
+ def whereis_exe(binary):
+ '''
+ Mock whereis_exe, so that it looks like
+ MacOS screencapture binary is present on the system.
+ '''
+ return binary == 'screencapture'
+
+ @staticmethod
+ def call(args):
+ '''
+ Mocked subprocess.call to check console parameters.
+ '''
+ assert len(args) == 2, len(args)
+ assert args[0] == 'screencapture', args
+ assert args[1] == join(
+ expanduser('~'), 'Pictures', 'screenshot.png'
+ ), args
+ with open(args[1], 'w') as scr:
+ scr.write('')
+
+
+class MockedXWD:
+ '''
+ Mocked object used instead of the console-like calling
+ of X11 xwd binary with parameters.
+ '''
+ @staticmethod
+ def whereis_exe(binary):
+ '''
+ Mock whereis_exe, so that it looks like
+ X11 xwd binary is present on the system.
+ '''
+ return binary == 'xwd'
+
+ @staticmethod
+ def call(args, stdout):
+ '''
+ Mocked subprocess.call to check console parameters.
+ '''
+ assert len(args) == 3, args
+ assert args[0] == 'xwd', args
+ assert args[1:] == ['-silent', '-root'], args
+ assert stdout.name == join(
+ expanduser('~'), 'Pictures', 'screenshot.xwd'
+ ), stdout.name
+ with open(stdout.name, 'w') as scr:
+ scr.write('')
+
+
+class TestScreenshot(unittest.TestCase):
+ '''
+ TestCase for plyer.screenshot.
+ '''
+
+ def setUp(self):
+ path = join(expanduser('~'), 'Pictures')
+ if not exists(path):
+ mkdir(path)
+
+ @PlatformTest('macosx')
+ def test_screenshot_screencapture(self):
+ '''
+ Test mocked MacOS screencapture for plyer.screenshot.
+ '''
+ scr = platform_import(
+ platform='macosx',
+ module_name='screenshot',
+ whereis_exe=MockedScreenCapture.whereis_exe
+ )
+
+ # such class exists in screenshot module
+ self.assertIn('OSXScreenshot', dir(scr))
+
+ # the required instance is created
+ scr = scr.instance()
+ self.assertIn('OSXScreenshot', str(scr))
+
+ # move capture from context manager to run without mock
+ with patch(target='subprocess.call', new=MockedScreenCapture.call):
+ self.assertIsNone(scr.capture())
+
+ self.assertTrue(exists(scr.file_path))
+ remove(scr.file_path)
+
+ @PlatformTest('linux')
+ def test_screenshot_xwd(self):
+ '''
+ Test mocked X11 xwd for plyer.screenshot.
+ '''
+ scr = platform_import(
+ platform='linux',
+ module_name='screenshot',
+ whereis_exe=MockedXWD.whereis_exe
+ )
+
+ # such class exists in screenshot module
+ self.assertIn('LinuxScreenshot', dir(scr))
+
+ # the required instance is created
+ scr = scr.instance()
+ self.assertIn('LinuxScreenshot', str(scr))
+
+ # move capture from context manager to run without mock
+ with patch(target='subprocess.call', new=MockedXWD.call):
+ self.assertIsNone(scr.capture())
+
+ self.assertTrue(exists(scr.file_path))
+ remove(scr.file_path)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/sbapp/plyer/tests/test_storagepath.py b/sbapp/plyer/tests/test_storagepath.py
new file mode 100644
index 00000000..35b8139e
--- /dev/null
+++ b/sbapp/plyer/tests/test_storagepath.py
@@ -0,0 +1,72 @@
+'''
+TestStoragePath
+===============
+
+Tested platforms:
+
+* macOS
+'''
+
+import unittest
+
+from plyer.tests.common import platform_import, PlatformTest
+
+
+class TestStoragePath(unittest.TestCase):
+ '''
+ TestCase for plyer.storagepath.
+ '''
+
+ @PlatformTest('macosx')
+ def test_storagepath_macosx(self):
+ '''
+ Test macOS for plyer.storagepath.
+ '''
+ storagepath = platform_import(
+ platform='macosx',
+ module_name='storagepath'
+ )
+
+ self.assertIn('OSXStoragePath', dir(storagepath))
+ storagepath = storagepath.instance()
+ self.assertIn('OSXStoragePath', str(storagepath))
+
+ path_format = 'file:///Users/'
+
+ self.assertIn(path_format, storagepath.get_home_dir())
+ self.assertIn('/', storagepath.get_root_dir())
+ self.assertIn(path_format, storagepath.get_documents_dir())
+ self.assertIn(path_format, storagepath.get_downloads_dir())
+ self.assertIn(path_format, storagepath.get_videos_dir())
+ self.assertIn(path_format, storagepath.get_music_dir())
+ self.assertIn(path_format, storagepath.get_pictures_dir())
+ self.assertIn(path_format, storagepath.get_application_dir())
+
+ @PlatformTest('win')
+ def test_storagepath_windows(self):
+ '''
+ Test win for plyer.storagepath.
+ '''
+ storagepath = platform_import(
+ platform='win',
+ module_name='storagepath'
+ )
+
+ self.assertIn('WinStoragePath', dir(storagepath))
+ storagepath = storagepath.instance()
+ self.assertIn('WinStoragePath', str(storagepath))
+
+ path_format = ':\\'
+
+ self.assertIn(path_format, storagepath.get_home_dir())
+ self.assertIn(path_format, storagepath.get_root_dir())
+ self.assertIn(path_format, storagepath.get_documents_dir())
+ self.assertIn(path_format, storagepath.get_downloads_dir())
+ self.assertIn(path_format, storagepath.get_videos_dir())
+ self.assertIn(path_format, storagepath.get_music_dir())
+ self.assertIn(path_format, storagepath.get_pictures_dir())
+ self.assertIn(path_format, storagepath.get_application_dir())
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/sbapp/plyer/tests/test_uniqueid.py b/sbapp/plyer/tests/test_uniqueid.py
new file mode 100644
index 00000000..629c840a
--- /dev/null
+++ b/sbapp/plyer/tests/test_uniqueid.py
@@ -0,0 +1,78 @@
+'''
+TestUniqueID
+============
+
+Tested platforms:
+
+* Windows
+'''
+
+import unittest
+from mock import patch, Mock
+from plyer.tests.common import PlatformTest, platform_import
+
+
+class TestUniqueID(unittest.TestCase):
+ '''
+ TestCase for plyer.uniqueid.
+ '''
+
+ def test_uniqueid(self):
+ '''
+ General all platform test for plyer.uniqueid.
+ '''
+ from plyer import uniqueid
+ self.assertTrue(len(uniqueid.id) > 0)
+
+ @PlatformTest('win')
+ def test_uniqueid_win(self):
+ '''
+ Test Windows API for plyer.uniqueid.
+ '''
+ try:
+ from winreg import (
+ HKEY_LOCAL_MACHINE as HKLM,
+ KEY_READ as READ, KEY_WOW64_64KEY as VIEW
+ )
+ except ImportError:
+ from _winreg import (
+ HKEY_LOCAL_MACHINE as HKLM,
+ KEY_READ as READ, KEY_WOW64_64KEY as VIEW
+ )
+
+ # mock the 'regedit' alias for winreg,
+ # see if the import passes and get the instance
+ regedit_mod = 'plyer.platforms.win.uniqueid.regedit'
+ with patch(target=regedit_mod):
+ uniqueid_ = platform_import(
+ platform='win',
+ module_name='uniqueid'
+ )
+ uniqueid = uniqueid_.instance()
+ self.assertIsInstance(uniqueid_.regedit, Mock)
+
+ # out of mocking block, regedit should be a winreg module
+ self.assertIsInstance(uniqueid_.regedit, type(unittest))
+
+ # OpenKey is supposed to return a handle to registry key
+ regedit_opkey = 'plyer.platforms.win.uniqueid.regedit.OpenKey'
+ with patch(target=regedit_opkey, return_value='unicorn') as opkey:
+
+ # QueryValueEx is supposed to return 2 packed values
+ # (key, type_id)
+ queryval = 'plyer.platforms.win.uniqueid.regedit.QueryValueEx'
+ retval = ('unique', None)
+ with patch(target=queryval, return_value=retval) as query:
+ uid = uniqueid.id
+ opkey.assert_called_once_with(
+ # key, subkey
+ HKLM, r'SOFTWARE\\Microsoft\\Cryptography',
+ # reserved integer (has to be 0 - zero), access mask
+ 0, READ | VIEW
+ )
+ query.assert_called_once_with('unicorn', 'MachineGuid')
+ self.assertEqual(uid, retval[0])
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/sbapp/plyer/tests/test_utils.py b/sbapp/plyer/tests/test_utils.py
new file mode 100644
index 00000000..3532b76b
--- /dev/null
+++ b/sbapp/plyer/tests/test_utils.py
@@ -0,0 +1,417 @@
+'''
+TestUtils
+=========
+
+Tested platforms:
+
+* Android
+* iOS
+* Windows
+* MacOS
+* Linux
+'''
+
+import unittest
+from mock import patch
+
+
+class TestUtils(unittest.TestCase):
+ '''
+ TestCase for plyer.utils.
+ '''
+
+ def cutter(self, part, string):
+ '''
+ Cut off a part of a string if it contains a substring,
+ otherwise raise an error.
+ '''
+ self.assertIn(part, string)
+ return string[len(part):]
+
+ def test_deprecated_function(self):
+ '''
+ Test printed out warning with @deprecated decorator
+ on a function without any arguments.
+ '''
+
+ from plyer.utils import deprecated
+
+ @deprecated
+ def function():
+ '''
+ Dummy deprecated function.
+ '''
+ return 1
+
+ with patch(target='warnings.warn') as stderr:
+ self.assertEqual(function(), 1)
+
+ args, _ = stderr.call_args_list[0]
+ args = args[0]
+ args = self.cutter('[WARNING] ', args)
+ args = self.cutter('deprecated function function', args)
+ args = self.cutter('test_utils.py', args)
+ args = self.cutter('Called from', args)
+ args = self.cutter('test_utils.py', args)
+ args = self.cutter('by test_deprecated_function().\n', args)
+
+ args, _ = stderr.call_args_list[1]
+ self.assertEqual(args, (
+ '''
+ Dummy deprecated function.
+ ''',
+ ))
+
+ def test_deprecated_function_arg(self):
+ '''
+ Test printed out warning with @deprecated decorator
+ on a function with arguments.
+ '''
+
+ from plyer.utils import deprecated
+
+ @deprecated
+ def function_with_arg(arg):
+ '''
+ Dummy deprecated function with arg.
+ '''
+ return arg
+
+ with patch(target='warnings.warn') as stderr:
+ self.assertEqual(function_with_arg(1), 1)
+
+ args, _ = stderr.call_args_list[0]
+ args = args[0]
+ args = self.cutter('[WARNING] ', args)
+ args = self.cutter('deprecated function function_with_arg', args)
+ args = self.cutter('test_utils.py', args)
+ args = self.cutter('Called from', args)
+ args = self.cutter('test_utils.py', args)
+ args = self.cutter('by test_deprecated_function_arg().\n', args)
+
+ args, _ = stderr.call_args_list[1]
+ self.assertEqual(args, (
+ '''
+ Dummy deprecated function with arg.
+ ''',
+ ))
+
+ def test_deprecated_function_kwarg(self):
+ '''
+ Test printed out warning with @deprecated decorator
+ on a function with keyword arguments.
+ '''
+
+ from plyer.utils import deprecated
+
+ @deprecated
+ def function_with_kwarg(kwarg):
+ '''
+ Dummy deprecated function with kwarg.
+ '''
+ return kwarg
+
+ with patch(target='warnings.warn') as stderr:
+ self.assertEqual(function_with_kwarg(kwarg=1), 1)
+
+ args, _ = stderr.call_args_list[0]
+ args = args[0]
+ args = self.cutter('[WARNING] ', args)
+ args = self.cutter('deprecated function function_with_kwarg', args)
+ args = self.cutter('test_utils.py', args)
+ args = self.cutter('Called from', args)
+ args = self.cutter('test_utils.py', args)
+ args = self.cutter('by test_deprecated_function_kwarg().\n', args)
+
+ args, _ = stderr.call_args_list[1]
+ self.assertEqual(args, (
+ '''
+ Dummy deprecated function with kwarg.
+ ''',
+ ))
+
+ def test_deprecated_class_method(self):
+ '''
+ Test printed out warning with @deprecated decorator
+ on a instance bound method.
+ '''
+
+ from plyer.utils import deprecated
+
+ class Class:
+ '''
+ Dummy class with deprecated method method.
+ '''
+ def __init__(self, *args, **kwargs):
+ self.args = args
+ self.kwargs = kwargs
+
+ @deprecated
+ def method(self):
+ '''
+ Dummy deprecated method.
+ '''
+ return (self.args, self.kwargs)
+
+ with patch(target='warnings.warn') as stderr:
+ args = (1, 2, 3)
+ kwargs = dict(x=1, y=2)
+
+ cls = Class(*args, **kwargs)
+ self.assertEqual(cls.method(), (args, kwargs))
+
+ args, kwargs = stderr.call_args_list[0]
+ args = args[0]
+ args = self.cutter('[WARNING] ', args)
+ args = self.cutter('deprecated function method', args)
+ args = self.cutter('test_utils.py', args)
+ args = self.cutter('Called from', args)
+ args = self.cutter('test_utils.py', args)
+ args = self.cutter('by test_deprecated_class_method().\n', args)
+
+ args, kwargs = stderr.call_args_list[1]
+ self.assertEqual(args, (
+ '''
+ Dummy deprecated method.
+ ''',
+ ))
+
+ def test_deprecated_class_static_none(self):
+ '''
+ Test printed out warning with @deprecated decorator
+ on a static method without arguments.
+ '''
+
+ from plyer.utils import deprecated
+
+ class Class:
+ '''
+ Dummy class with deprecated static method.
+ '''
+ args = None
+ kwargs = None
+
+ def __init__(self, *args, **kwargs):
+ Class.args = args
+ Class.kwargs = kwargs
+
+ @staticmethod
+ @deprecated
+ def static():
+ '''
+ Dummy deprecated static method.
+ '''
+ return (Class.args, Class.kwargs)
+
+ with patch(target='warnings.warn') as stderr:
+ self.assertEqual(Class.static(), (None, None))
+
+ args, _ = stderr.call_args_list[0]
+ args = args[0]
+ args = self.cutter('[WARNING] ', args)
+ args = self.cutter('deprecated function static', args)
+ args = self.cutter('test_utils.py', args)
+ args = self.cutter('Called from', args)
+ args = self.cutter('test_utils.py', args)
+ args = self.cutter(
+ 'by test_deprecated_class_static_none().\n', args
+ )
+
+ args, _ = stderr.call_args_list[1]
+ self.assertEqual(args, (
+ '''
+ Dummy deprecated static method.
+ ''',
+ ))
+
+ def test_deprecated_class_static_argskwargs(self):
+ '''
+ Test printed out warning with @deprecated decorator
+ on a static method with arguments and keyword argument.
+ '''
+
+ from plyer.utils import deprecated
+
+ class Class:
+ '''
+ Dummy class with deprecated static method.
+ '''
+ args = None
+ kwargs = None
+
+ def __init__(self, *args, **kwargs):
+ Class.args = args
+ Class.kwargs = kwargs
+
+ @staticmethod
+ @deprecated
+ def static():
+ '''
+ Dummy deprecated static method.
+ '''
+ return (Class.args, Class.kwargs)
+
+ with patch(target='warnings.warn') as stderr:
+ args = (1, 2, 3)
+ kwargs = dict(x=1, y=2)
+
+ cls = Class(*args, **kwargs)
+ self.assertEqual(cls.static(), (args, kwargs))
+
+ args, kwargs = stderr.call_args_list[0]
+ args = args[0]
+ args = self.cutter('[WARNING] ', args)
+ args = self.cutter('deprecated function static', args)
+ args = self.cutter('test_utils.py', args)
+ args = self.cutter('Called from', args)
+ args = self.cutter('test_utils.py', args)
+ args = self.cutter(
+ 'by test_deprecated_class_static_argskwargs().\n', args
+ )
+
+ args, kwargs = stderr.call_args_list[1]
+ self.assertEqual(args, (
+ '''
+ Dummy deprecated static method.
+ ''',
+ ))
+
+ def test_deprecated_class_clsmethod(self):
+ '''
+ Test printed out warning with @deprecated decorator
+ on a class bound method.
+ '''
+
+ from plyer.utils import deprecated
+
+ class Class:
+ '''
+ Dummy class with deprecated class method.
+ '''
+ args = None
+ kwargs = None
+
+ @classmethod
+ @deprecated
+ def clsmethod(cls):
+ '''
+ Dummy deprecated class method.
+ '''
+ return (cls.args, cls.kwargs)
+
+ with patch(target='warnings.warn') as stderr:
+ self.assertEqual(Class.clsmethod(), (None, None))
+
+ args, _ = stderr.call_args_list[0]
+ args = args[0]
+ args = self.cutter('[WARNING] ', args)
+ args = self.cutter('deprecated function clsmethod', args)
+ args = self.cutter('test_utils.py', args)
+ args = self.cutter('Called from', args)
+ args = self.cutter('test_utils.py', args)
+ args = self.cutter('by test_deprecated_class_clsmethod().\n', args)
+
+ args, _ = stderr.call_args_list[1]
+ self.assertEqual(args, (
+ '''
+ Dummy deprecated class method.
+ ''',
+ ))
+
+ def test_deprecated_class(self):
+ '''
+ Test printed out warning with @deprecated decorator on a class.
+ '''
+
+ from plyer.utils import deprecated
+
+ @deprecated
+ class Class:
+ '''
+ Dummy deprecated class.
+ '''
+ def __init__(self, *args, **kwargs):
+ self.args = args
+ self.kwargs = kwargs
+
+ with patch(target='warnings.warn') as stderr:
+ args = (1, 2, 3)
+ kwargs = dict(x=1, y=2)
+
+ cls = Class(*args, **kwargs)
+ self.assertIsInstance(cls, Class)
+ self.assertEqual(cls.args, args)
+ self.assertEqual(cls.kwargs, kwargs)
+
+ args, _ = stderr.call_args_list[0]
+ args = args[0]
+ args = self.cutter('[WARNING] ', args)
+ args = self.cutter('Creating an instance', args)
+ args = self.cutter('deprecated class Class in', args)
+ args = self.cutter(__name__, args)
+ args = self.cutter('Called from', args)
+ args = self.cutter('test_utils.py', args)
+ args = self.cutter('by test_deprecated_class().\n', args)
+
+ args, kwargs = stderr.call_args_list[1]
+ self.assertEqual(args, (
+ '''
+ Dummy deprecated class.
+ ''',
+ ))
+
+ def test_deprecated_class_inherited(self):
+ '''
+ Test printed out warning with @deprecated decorator on a class
+ which inherits from a deprecated class.
+ '''
+
+ from plyer.utils import deprecated
+
+ @deprecated
+ class Class:
+ '''
+ Dummy deprecated class.
+ '''
+ def __init__(self, *args, **kwargs):
+ self.args = args
+ self.kwargs = kwargs
+
+ class Inherited(Class):
+ '''
+ Dummy class inheriting from a dummy deprecated class.
+ '''
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.args = args
+ self.kwargs = kwargs
+
+ with patch(target='warnings.warn') as stderr:
+ args = (1, 2, 3)
+ kwargs = dict(x=1, y=2)
+
+ cls = Inherited(*args, **kwargs)
+ self.assertIsInstance(cls, Inherited)
+ self.assertEqual(cls.args, args)
+ self.assertEqual(cls.kwargs, kwargs)
+
+ args, _ = stderr.call_args_list[0]
+ args = args[0]
+ args = self.cutter('[WARNING] ', args)
+ args = self.cutter('Creating an instance', args)
+ args = self.cutter('deprecated class Class in', args)
+ args = self.cutter(__name__, args)
+ args = self.cutter('Called from', args)
+ args = self.cutter('test_utils.py', args)
+ args = self.cutter('by test_deprecated_class_inherited().\n', args)
+
+ args, kwargs = stderr.call_args_list[1]
+ self.assertEqual(args, (
+ '''
+ Dummy deprecated class.
+ ''',
+ ))
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/sbapp/plyer/tools/pep8checker/pep8.py b/sbapp/plyer/tools/pep8checker/pep8.py
new file mode 100644
index 00000000..a2f1f2cd
--- /dev/null
+++ b/sbapp/plyer/tools/pep8checker/pep8.py
@@ -0,0 +1,1953 @@
+#!/usr/bin/env python
+# pep8.py - Check Python source code formatting, according to PEP 8
+# Copyright (C) 2006 Johann C. Rocholl <johann@rocholl.net>
+#
+# Permission is hereby granted, free of charge, to any person
+# obtaining a copy of this software and associated documentation files
+# (the "Software"), to deal in the Software without restriction,
+# including without limitation the rights to use, copy, modify, merge,
+# publish, distribute, sublicense, and/or sell copies of the Software,
+# and to permit persons to whom the Software is furnished to do so,
+# subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+# flake8: noqa
+r"""
+Check Python source code formatting, according to PEP 8:
+http://www.python.org/dev/peps/pep-0008/
+
+For usage and a list of options, try this:
+$ python pep8.py -h
+
+This program and its regression test suite live here:
+http://github.com/jcrocholl/pep8
+
+Groups of errors and warnings:
+E errors
+W warnings
+100 indentation
+200 whitespace
+300 blank lines
+400 imports
+500 line length
+600 deprecation
+700 statements
+900 syntax error
+
+You can add checks to this program by writing plugins. Each plugin is
+a simple function that is called for each line of source code, either
+physical or logical.
+
+Physical line:
+- Raw line of text from the input file.
+
+Logical line:
+- Multi-line statements converted to a single line.
+- Stripped left and right.
+- Contents of strings replaced with 'xxx' of same length.
+- Comments removed.
+
+The check function requests physical or logical lines by the name of
+the first argument:
+
+def maximum_line_length(physical_line)
+def extraneous_whitespace(logical_line)
+def blank_lines(logical_line, blank_lines, indent_level, line_number)
+
+The last example above demonstrates how check plugins can request
+additional information with extra arguments. All attributes of the
+Checker object are available. Some examples:
+
+lines: a list of the raw lines from the input file
+tokens: the tokens that contribute to this logical line
+line_number: line number in the input file
+blank_lines: blank lines before this one
+indent_char: first indentation character in this file (' ' or '\t')
+indent_level: indentation (with tabs expanded to multiples of 8)
+previous_indent_level: indentation on previous line
+previous_logical: previous logical line
+
+The docstring of each check function shall be the relevant part of
+text from PEP 8. It is printed if the user enables --show-pep8.
+Several docstrings contain examples directly from the PEP 8 document.
+
+Okay: spam(ham[1], {eggs: 2})
+E201: spam( ham[1], {eggs: 2})
+
+These examples are verified automatically when pep8.py is run with the
+--doctest option. You can add examples for your own check functions.
+The format is simple: "Okay" or error/warning code followed by colon
+and space, the rest of the line is example source code. If you put 'r'
+before the docstring, you can use \n for newline, \t for tab and \s
+for space.
+
+"""
+
+__version__ = '1.3.3'
+
+import os
+import sys
+import re
+import time
+import inspect
+import keyword
+import tokenize
+from optparse import OptionParser
+from fnmatch import fnmatch
+try:
+ from ConfigParser import RawConfigParser
+except ImportError:
+ from configparser import RawConfigParser
+ from io import TextIOWrapper
+
+DEFAULT_EXCLUDE = '.svn,CVS,.bzr,.hg,.git'
+DEFAULT_IGNORE = 'E24'
+if sys.platform == 'win32':
+ DEFAULT_CONFIG = os.path.expanduser(r'~\.pep8')
+else:
+ DEFAULT_CONFIG = os.path.join(os.getenv('XDG_CONFIG_HOME') or
+ os.path.expanduser('~/.config'), 'pep8')
+MAX_LINE_LENGTH = 80
+REPORT_FORMAT = {
+ 'default': '%(path)s:%(row)d:%(col)d: %(code)s %(text)s',
+ 'pylint': '%(path)s:%(row)d: [%(code)s] %(text)s',
+}
+
+
+SINGLETONS = frozenset(['False', 'None', 'True'])
+KEYWORDS = frozenset(keyword.kwlist + ['print']) - SINGLETONS
+BINARY_OPERATORS = frozenset([
+ '**=', '*=', '+=', '-=', '!=', '<>',
+ '%=', '^=', '&=', '|=', '==', '/=', '//=', '<=', '>=', '<<=', '>>=',
+ '%', '^', '&', '|', '=', '/', '//', '<', '>', '<<'])
+UNARY_OPERATORS = frozenset(['>>', '**', '*', '+', '-'])
+OPERATORS = BINARY_OPERATORS | UNARY_OPERATORS
+WHITESPACE = frozenset(' \t')
+SKIP_TOKENS = frozenset([tokenize.COMMENT, tokenize.NL, tokenize.NEWLINE,
+ tokenize.INDENT, tokenize.DEDENT])
+BENCHMARK_KEYS = ['directories', 'files', 'logical lines', 'physical lines']
+
+INDENT_REGEX = re.compile(r'([ \t]*)')
+RAISE_COMMA_REGEX = re.compile(r'raise\s+\w+\s*(,)')
+RERAISE_COMMA_REGEX = re.compile(r'raise\s+\w+\s*,\s*\w+\s*,\s*\w+')
+SELFTEST_REGEX = re.compile(r'(Okay|[EW]\d{3}):\s(.*)')
+ERRORCODE_REGEX = re.compile(r'[EW]\d{3}')
+DOCSTRING_REGEX = re.compile(r'u?r?["\']')
+EXTRANEOUS_WHITESPACE_REGEX = re.compile(r'[[({] | []}),;:]')
+WHITESPACE_AFTER_COMMA_REGEX = re.compile(r'[,;:]\s*(?: |\t)')
+COMPARE_SINGLETON_REGEX = re.compile(r'([=!]=)\s*(None|False|True)')
+COMPARE_TYPE_REGEX = re.compile(r'([=!]=|is|is\s+not)\s*type(?:s\.(\w+)Type'
+ r'|\(\s*(\(\s*\)|[^)]*[^ )])\s*\))')
+KEYWORD_REGEX = re.compile(r'(?:[^\s])(\s*)\b(?:%s)\b(\s*)' %
+ r'|'.join(KEYWORDS))
+OPERATOR_REGEX = re.compile(r'(?:[^\s])(\s*)(?:[-+*/|!<=>%&^]+)(\s*)')
+LAMBDA_REGEX = re.compile(r'\blambda\b')
+HUNK_REGEX = re.compile(r'^@@ -\d+,\d+ \+(\d+),(\d+) @@.*$')
+
+# Work around Python < 2.6 behaviour, which does not generate NL after
+# a comment which is on a line by itself.
+COMMENT_WITH_NL = tokenize.generate_tokens(['#\n'].pop).send(None)[1] == '#\n'
+
+
+##############################################################################
+# Plugins (check functions) for physical lines
+##############################################################################
+
+
+def tabs_or_spaces(physical_line, indent_char):
+ r"""
+ Never mix tabs and spaces.
+
+ The most popular way of indenting Python is with spaces only. The
+ second-most popular way is with tabs only. Code indented with a mixture
+ of tabs and spaces should be converted to using spaces exclusively. When
+ invoking the Python command line interpreter with the -t option, it issues
+ warnings about code that illegally mixes tabs and spaces. When using -tt
+ these warnings become errors. These options are highly recommended!
+
+ Okay: if a == 0:\n a = 1\n b = 1
+ E101: if a == 0:\n a = 1\n\tb = 1
+ """
+ indent = INDENT_REGEX.match(physical_line).group(1)
+ for offset, char in enumerate(indent):
+ if char != indent_char:
+ return offset, "E101 indentation contains mixed spaces and tabs"
+
+
+def tabs_obsolete(physical_line):
+ r"""
+ For new projects, spaces-only are strongly recommended over tabs. Most
+ editors have features that make this easy to do.
+
+ Okay: if True:\n return
+ W191: if True:\n\treturn
+ """
+ indent = INDENT_REGEX.match(physical_line).group(1)
+ if '\t' in indent:
+ return indent.index('\t'), "W191 indentation contains tabs"
+
+
+def trailing_whitespace(physical_line):
+ r"""
+ JCR: Trailing whitespace is superfluous.
+ FBM: Except when it occurs as part of a blank line (i.e. the line is
+ nothing but whitespace). According to Python docs[1] a line with only
+ whitespace is considered a blank line, and is to be ignored. However,
+ matching a blank line to its indentation level avoids mistakenly
+ terminating a multi-line statement (e.g. class declaration) when
+ pasting code into the standard Python interpreter.
+
+ [1] http://docs.python.org/reference/lexical_analysis.html#blank-lines
+
+ The warning returned varies on whether the line itself is blank, for easier
+ filtering for those who want to indent their blank lines.
+
+ Okay: spam(1)
+ W291: spam(1)\s
+ W293: class Foo:\n \n bang = 12
+ """
+ physical_line = physical_line.rstrip('\n') # chr(10), newline
+ physical_line = physical_line.rstrip('\r') # chr(13), carriage return
+ physical_line = physical_line.rstrip('\x0c') # chr(12), form feed, ^L
+ stripped = physical_line.rstrip(' \t\v')
+ if physical_line != stripped:
+ if stripped:
+ return len(stripped), "W291 trailing whitespace"
+ else:
+ return 0, "W293 blank line contains whitespace"
+
+
+# def trailing_blank_lines(physical_line, lines, line_number):
+# r"""
+# JCR: Trailing blank lines are superfluous.
+#
+# Okay: spam(1)
+# W391: spam(1)\n
+# """
+# if not physical_line.rstrip() and line_number == len(lines):
+# return 0, "W391 blank line at end of file"
+
+
+def missing_newline(physical_line):
+ """
+ JCR: The last line should have a newline.
+
+ Reports warning W292.
+ """
+ if physical_line.rstrip() == physical_line:
+ return len(physical_line), "W292 no newline at end of file"
+
+
+def maximum_line_length(physical_line, max_line_length):
+ """
+ Limit all lines to a maximum of 79 characters.
+
+ There are still many devices around that are limited to 80 character
+ lines; plus, limiting windows to 80 characters makes it possible to have
+ several windows side-by-side. The default wrapping on such devices looks
+ ugly. Therefore, please limit all lines to a maximum of 79 characters.
+ For flowing long blocks of text (docstrings or comments), limiting the
+ length to 72 characters is recommended.
+
+ Reports error E501.
+ """
+ line = physical_line.rstrip()
+ length = len(line)
+ if length > max_line_length:
+ if hasattr(line, 'decode'): # Python 2
+ # The line could contain multi-byte characters
+ try:
+ length = len(line.decode('utf-8'))
+ except UnicodeError:
+ pass
+ if length > max_line_length:
+ return (max_line_length, "E501 line too long "
+ "(%d > %d characters)" % (length, max_line_length))
+
+
+##############################################################################
+# Plugins (check functions) for logical lines
+##############################################################################
+
+
+def blank_lines(logical_line, blank_lines, indent_level, line_number,
+ previous_logical, previous_indent_level):
+ r"""
+ Separate top-level function and class definitions with two blank lines.
+
+ Method definitions inside a class are separated by a single blank line.
+
+ Extra blank lines may be used (sparingly) to separate groups of related
+ functions. Blank lines may be omitted between a bunch of related
+ one-liners (e.g. a set of dummy implementations).
+
+ Use blank lines in functions, sparingly, to indicate logical sections.
+
+ Okay: def a():\n pass\n\n\ndef b():\n pass
+ Okay: def a():\n pass\n\n\n# Foo\n# Bar\n\ndef b():\n pass
+
+ E301: class Foo:\n b = 0\n def bar():\n pass
+ E302: def a():\n pass\n\ndef b(n):\n pass
+ E303: def a():\n pass\n\n\n\ndef b(n):\n pass
+ E303: def a():\n\n\n\n pass
+ E304: @decorator\n\ndef a():\n pass
+ """
+ if line_number == 1:
+ return # Don't expect blank lines before the first line
+ if previous_logical.startswith('@'):
+ if blank_lines:
+ yield 0, "E304 blank lines found after function decorator"
+ elif blank_lines > 2 or (indent_level and blank_lines == 2):
+ yield 0, "E303 too many blank lines (%d)" % blank_lines
+ elif logical_line.startswith(('def ', 'class ', '@')):
+ if indent_level:
+ if not (blank_lines or previous_indent_level < indent_level or
+ DOCSTRING_REGEX.match(previous_logical)):
+ yield 0, "E301 expected 1 blank line, found 0"
+ elif blank_lines != 2:
+ yield 0, "E302 expected 2 blank lines, found %d" % blank_lines
+
+
+def extraneous_whitespace(logical_line):
+ """
+ Avoid extraneous whitespace in the following situations:
+
+ - Immediately inside parentheses, brackets or braces.
+
+ - Immediately before a comma, semicolon, or colon.
+
+ Okay: spam(ham[1], {eggs: 2})
+ E201: spam( ham[1], {eggs: 2})
+ E201: spam(ham[ 1], {eggs: 2})
+ E201: spam(ham[1], { eggs: 2})
+ E202: spam(ham[1], {eggs: 2} )
+ E202: spam(ham[1 ], {eggs: 2})
+ E202: spam(ham[1], {eggs: 2 })
+
+ E203: if x == 4: print x, y; x, y = y , x
+ E203: if x == 4: print x, y ; x, y = y, x
+ E203: if x == 4 : print x, y; x, y = y, x
+ """
+ line = logical_line
+ for match in EXTRANEOUS_WHITESPACE_REGEX.finditer(line):
+ text = match.group()
+ char = text.strip()
+ found = match.start()
+ if text == char + ' ':
+ # assert char in '([{'
+ yield found + 1, "E201 whitespace after '%s'" % char
+ elif line[found - 1] != ',':
+ code = ('E202' if char in '}])' else 'E203') # if char in ',;:'
+ yield found, "%s whitespace before '%s'" % (code, char)
+
+
+def whitespace_around_keywords(logical_line):
+ r"""
+ Avoid extraneous whitespace around keywords.
+
+ Okay: True and False
+ E271: True and False
+ E272: True and False
+ E273: True and\tFalse
+ E274: True\tand False
+ """
+ for match in KEYWORD_REGEX.finditer(logical_line):
+ before, after = match.groups()
+
+ if '\t' in before:
+ yield match.start(1), "E274 tab before keyword"
+ elif len(before) > 1:
+ yield match.start(1), "E272 multiple spaces before keyword"
+
+ if '\t' in after:
+ yield match.start(2), "E273 tab after keyword"
+ elif len(after) > 1:
+ yield match.start(2), "E271 multiple spaces after keyword"
+
+
+def missing_whitespace(logical_line):
+ """
+ JCR: Each comma, semicolon or colon should be followed by whitespace.
+
+ Okay: [a, b]
+ Okay: (3,)
+ Okay: a[1:4]
+ Okay: a[:4]
+ Okay: a[1:]
+ Okay: a[1:4:2]
+ E231: ['a','b']
+ E231: foo(bar,baz)
+ """
+ line = logical_line
+ for index in range(len(line) - 1):
+ char = line[index]
+ if char in ',;:' and line[index + 1] not in WHITESPACE:
+ before = line[:index]
+ if char == ':' and before.count('[') > before.count(']'):
+ continue # Slice syntax, no space required
+ if char == ',' and line[index + 1] == ')':
+ continue # Allow tuple with only one element: (3,)
+ yield index, "E231 missing whitespace after '%s'" % char
+
+
+def indentation(logical_line, previous_logical, indent_char,
+ indent_level, previous_indent_level):
+ r"""
+ Use 4 spaces per indentation level.
+
+ For really old code that you don't want to mess up, you can continue to
+ use 8-space tabs.
+
+ Okay: a = 1
+ Okay: if a == 0:\n a = 1
+ E111: .....a = 1
+
+ Okay: for item in items:\n pass
+ E112: for item in items:\npass
+
+ Okay: a = 1\nb = 2
+ E113: a = 1\n b = 2
+ """
+ if indent_char == ' ' and indent_level % 4:
+ yield 0, "E111 indentation is not a multiple of four"
+ indent_expect = previous_logical.endswith(':')
+ if indent_expect and indent_level <= previous_indent_level:
+ yield 0, "E112 expected an indented block"
+ if indent_level > previous_indent_level and not indent_expect:
+ yield 0, "E113 unexpected indentation"
+
+
+def continuation_line_indentation(logical_line, tokens, indent_level, verbose):
+ r"""
+ Continuation lines should align wrapped elements either vertically using
+ Python's implicit line joining inside parentheses, brackets and braces, or
+ using a hanging indent.
+
+ When using a hanging indent the following considerations should be applied:
+
+ - there should be no arguments on the first line, and
+
+ - further indentation should be used to clearly distinguish itself as a
+ continuation line.
+
+ Okay: a = (\n)
+ E123: a = (\n )
+
+ Okay: a = (\n 42)
+ E121: a = (\n 42)
+ E122: a = (\n42)
+ E123: a = (\n 42\n )
+ E124: a = (24,\n 42\n)
+ E125: if (a or\n b):\n pass
+ E126: a = (\n 42)
+ E127: a = (24,\n 42)
+ E128: a = (24,\n 42)
+ """
+ first_row = tokens[0][2][0]
+ nrows = 1 + tokens[-1][2][0] - first_row
+ if nrows == 1:
+ return
+
+ # indent_next tells us whether the next block is indented; assuming
+ # that it is indented by 4 spaces, then we should not allow 4-space
+ # indents on the final continuation line; in turn, some other
+ # indents are allowed to have an extra 4 spaces.
+ indent_next = logical_line.endswith(':')
+
+ row = depth = 0
+ # remember how many brackets were opened on each line
+ parens = [0] * nrows
+ # relative indents of physical lines
+ rel_indent = [0] * nrows
+ # visual indents
+ indent = [indent_level]
+ indent_chances = {}
+ last_indent = (0, 0)
+ if verbose >= 3:
+ print((">>> " + tokens[0][4].rstrip()))
+
+ for token_type, text, start, end, line in tokens:
+ newline = row < start[0] - first_row
+ if newline:
+ row = start[0] - first_row
+ newline = (not last_token_multiline and
+ token_type not in (tokenize.NL, tokenize.NEWLINE))
+
+ if newline:
+ # this is the beginning of a continuation line.
+ last_indent = start
+ if verbose >= 3:
+ print(("... " + line.rstrip()))
+
+ # record the initial indent.
+ rel_indent[row] = start[1] - indent_level
+
+ if depth:
+ # a bracket expression in a continuation line.
+ # find the line that it was opened on
+ for open_row in range(row - 1, -1, -1):
+ if parens[open_row]:
+ break
+ else:
+ # an unbracketed continuation line (ie, backslash)
+ open_row = 0
+ hang = rel_indent[row] - rel_indent[open_row]
+ visual_indent = indent_chances.get(start[1])
+
+ if token_type == tokenize.OP and text in ']})':
+ # this line starts with a closing bracket
+ if indent[depth]:
+ if start[1] != indent[depth]:
+ yield (start, 'E124 closing bracket does not match '
+ 'visual indentation')
+ elif hang:
+ yield (start, 'E123 closing bracket does not match '
+ 'indentation of opening bracket\'s line')
+ elif visual_indent is True:
+ # visual indent is verified
+ if not indent[depth]:
+ indent[depth] = start[1]
+ elif visual_indent in (text, str):
+ # ignore token lined up with matching one from a previous line
+ pass
+ elif indent[depth] and start[1] < indent[depth]:
+ # visual indent is broken
+ yield (start, 'E128 continuation line '
+ 'under-indented for visual indent')
+ elif hang == 4 or (indent_next and rel_indent[row] == 8):
+ # hanging indent is verified
+ pass
+ else:
+ # indent is broken
+ if hang <= 0:
+ error = 'E122', 'missing indentation or outdented'
+ elif indent[depth]:
+ error = 'E127', 'over-indented for visual indent'
+ elif hang % 4:
+ error = 'E121', 'indentation is not a multiple of four'
+ else:
+ error = 'E126', 'over-indented for hanging indent'
+ yield start, "%s continuation line %s" % error
+
+ # look for visual indenting
+ if parens[row] and token_type != tokenize.NL and not indent[depth]:
+ indent[depth] = start[1]
+ indent_chances[start[1]] = True
+ if verbose >= 4:
+ print(("bracket depth %s indent to %s" % (depth, start[1])))
+ # deal with implicit string concatenation
+ elif token_type == tokenize.STRING or text in ('u', 'ur', 'b', 'br'):
+ indent_chances[start[1]] = str
+
+ # keep track of bracket depth
+ if token_type == tokenize.OP:
+ if text in '([{':
+ depth += 1
+ indent.append(0)
+ parens[row] += 1
+ if verbose >= 4:
+ print(("bracket depth %s seen, col %s, visual min = %s" %
+ (depth, start[1], indent[depth])))
+ elif text in ')]}' and depth > 0:
+ # parent indents should not be more than this one
+ prev_indent = indent.pop() or last_indent[1]
+ for d in range(depth):
+ if indent[d] > prev_indent:
+ indent[d] = 0
+ for ind in list(indent_chances):
+ if ind >= prev_indent:
+ del indent_chances[ind]
+ depth -= 1
+ if depth:
+ indent_chances[indent[depth]] = True
+ for idx in range(row, -1, -1):
+ if parens[idx]:
+ parens[idx] -= 1
+ break
+ assert len(indent) == depth + 1
+ if start[1] not in indent_chances:
+ # allow to line up tokens
+ indent_chances[start[1]] = text
+
+ last_token_multiline = (start[0] != end[0])
+
+ if indent_next and rel_indent[-1] == 4:
+ yield (last_indent, "E125 continuation line does not distinguish "
+ "itself from next logical line")
+
+
+def whitespace_before_parameters(logical_line, tokens):
+ """
+ Avoid extraneous whitespace in the following situations:
+
+ - Immediately before the open parenthesis that starts the argument
+ list of a function call.
+
+ - Immediately before the open parenthesis that starts an indexing or
+ slicing.
+
+ Okay: spam(1)
+ E211: spam (1)
+
+ Okay: dict['key'] = list[index]
+ E211: dict ['key'] = list[index]
+ E211: dict['key'] = list [index]
+ """
+ prev_type = tokens[0][0]
+ prev_text = tokens[0][1]
+ prev_end = tokens[0][3]
+ for index in range(1, len(tokens)):
+ token_type, text, start, end, line = tokens[index]
+ if (token_type == tokenize.OP and
+ text in '([' and
+ start != prev_end and
+ (prev_type == tokenize.NAME or prev_text in '}])') and
+ # Syntax "class A (B):" is allowed, but avoid it
+ (index < 2 or tokens[index - 2][1] != 'class') and
+ # Allow "return (a.foo for a in range(5))"
+ not keyword.iskeyword(prev_text)):
+ yield prev_end, "E211 whitespace before '%s'" % text
+ prev_type = token_type
+ prev_text = text
+ prev_end = end
+
+
+def whitespace_around_operator(logical_line):
+ r"""
+ Avoid extraneous whitespace in the following situations:
+
+ - More than one space around an assignment (or other) operator to
+ align it with another.
+
+ Okay: a = 12 + 3
+ E221: a = 4 + 5
+ E222: a = 4 + 5
+ E223: a = 4\t+ 5
+ E224: a = 4 +\t5
+ """
+ for match in OPERATOR_REGEX.finditer(logical_line):
+ before, after = match.groups()
+
+ if '\t' in before:
+ yield match.start(1), "E223 tab before operator"
+ elif len(before) > 1:
+ yield match.start(1), "E221 multiple spaces before operator"
+
+ if '\t' in after:
+ yield match.start(2), "E224 tab after operator"
+ elif len(after) > 1:
+ yield match.start(2), "E222 multiple spaces after operator"
+
+
+def missing_whitespace_around_operator(logical_line, tokens):
+ r"""
+ - Always surround these binary operators with a single space on
+ either side: assignment (=), augmented assignment (+=, -= etc.),
+ comparisons (==, <, >, !=, <>, <=, >=, in, not in, is, is not),
+ Booleans (and, or, not).
+
+ - Use spaces around arithmetic operators.
+
+ Okay: i = i + 1
+ Okay: submitted += 1
+ Okay: x = x * 2 - 1
+ Okay: hypot2 = x * x + y * y
+ Okay: c = (a + b) * (a - b)
+ Okay: foo(bar, key='word', *args, **kwargs)
+ Okay: baz(**kwargs)
+ Okay: negative = -1
+ Okay: spam(-1)
+ Okay: alpha[:-i]
+ Okay: if not -5 < x < +5:\n pass
+ Okay: lambda *args, **kw: (args, kw)
+
+ E225: i=i+1
+ E225: submitted +=1
+ E225: x = x*2 - 1
+ E225: hypot2 = x*x + y*y
+ E225: c = (a+b) * (a-b)
+ E225: c = alpha -4
+ E225: z = x **y
+ """
+ parens = 0
+ need_space = False
+ prev_type = tokenize.OP
+ prev_text = prev_end = None
+ for token_type, text, start, end, line in tokens:
+ if token_type in (tokenize.NL, tokenize.NEWLINE, tokenize.ERRORTOKEN):
+ # ERRORTOKEN is triggered by backticks in Python 3000
+ continue
+ if text in ('(', 'lambda'):
+ parens += 1
+ elif text == ')':
+ parens -= 1
+ if need_space:
+ if start != prev_end:
+ need_space = False
+ elif text == '>' and prev_text in ('<', '-'):
+ # Tolerate the "<>" operator, even if running Python 3
+ # Deal with Python 3's annotated return value "->"
+ pass
+ else:
+ yield prev_end, "E225 missing whitespace around operator"
+ need_space = False
+ elif token_type == tokenize.OP and prev_end is not None:
+ if text == '=' and parens:
+ # Allow keyword args or defaults: foo(bar=None).
+ pass
+ elif text in BINARY_OPERATORS:
+ need_space = True
+ elif text in UNARY_OPERATORS:
+ # Allow unary operators: -123, -x, +1.
+ # Allow argument unpacking: foo(*args, **kwargs).
+ if prev_type == tokenize.OP:
+ if prev_text in '}])':
+ need_space = True
+ elif prev_type == tokenize.NAME:
+ if prev_text not in KEYWORDS:
+ need_space = True
+ elif prev_type not in SKIP_TOKENS:
+ need_space = True
+ if need_space and start == prev_end:
+ yield prev_end, "E225 missing whitespace around operator"
+ need_space = False
+ prev_type = token_type
+ prev_text = text
+ prev_end = end
+
+
+def whitespace_around_comma(logical_line):
+ r"""
+ Avoid extraneous whitespace in the following situations:
+
+ - More than one space around an assignment (or other) operator to
+ align it with another.
+
+ Note: these checks are disabled by default
+
+ Okay: a = (1, 2)
+ E241: a = (1, 2)
+ E242: a = (1,\t2)
+ """
+ line = logical_line
+ for m in WHITESPACE_AFTER_COMMA_REGEX.finditer(line):
+ found = m.start() + 1
+ if '\t' in m.group():
+ yield found, "E242 tab after '%s'" % m.group()[0]
+ else:
+ yield found, "E241 multiple spaces after '%s'" % m.group()[0]
+
+
+def whitespace_around_named_parameter_equals(logical_line, tokens):
+ """
+ Don't use spaces around the '=' sign when used to indicate a
+ keyword argument or a default parameter value.
+
+ Okay: def complex(real, imag=0.0):
+ Okay: return magic(r=real, i=imag)
+ Okay: boolean(a == b)
+ Okay: boolean(a != b)
+ Okay: boolean(a <= b)
+ Okay: boolean(a >= b)
+
+ E251: def complex(real, imag = 0.0):
+ E251: return magic(r = real, i = imag)
+ """
+ parens = 0
+ no_space = False
+ prev_end = None
+ for token_type, text, start, end, line in tokens:
+ if no_space:
+ no_space = False
+ if start != prev_end:
+ yield (prev_end,
+ "E251 no spaces around keyword / parameter equals")
+ elif token_type == tokenize.OP:
+ if text == '(':
+ parens += 1
+ elif text == ')':
+ parens -= 1
+ elif parens and text == '=':
+ no_space = True
+ if start != prev_end:
+ yield (prev_end,
+ "E251 no spaces around keyword / parameter equals")
+ prev_end = end
+
+
+def whitespace_before_inline_comment(logical_line, tokens):
+ """
+ Separate inline comments by at least two spaces.
+
+ An inline comment is a comment on the same line as a statement. Inline
+ comments should be separated by at least two spaces from the statement.
+ They should start with a # and a single space.
+
+ Okay: x = x + 1 # Increment x
+ Okay: x = x + 1 # Increment x
+ E261: x = x + 1 # Increment x
+ E262: x = x + 1 #Increment x
+ E262: x = x + 1 # Increment x
+ """
+ prev_end = (0, 0)
+ for token_type, text, start, end, line in tokens:
+ if token_type == tokenize.COMMENT:
+ if not line[:start[1]].strip():
+ continue
+ if prev_end[0] == start[0] and start[1] < prev_end[1] + 2:
+ yield (prev_end,
+ "E261 at least two spaces before inline comment")
+ if text.startswith('# ') or not text.startswith('# '):
+ yield start, "E262 inline comment should start with '# '"
+ elif token_type != tokenize.NL:
+ prev_end = end
+
+
+def imports_on_separate_lines(logical_line):
+ r"""
+ Imports should usually be on separate lines.
+
+ Okay: import os\nimport sys
+ E401: import sys, os
+
+ Okay: from subprocess import Popen, PIPE
+ Okay: from myclas import MyClass
+ Okay: from foo.bar.yourclass import YourClass
+ Okay: import myclass
+ Okay: import foo.bar.yourclass
+ """
+ line = logical_line
+ if line.startswith('import '):
+ found = line.find(',')
+ if -1 < found:
+ yield found, "E401 multiple imports on one line"
+
+
+def compound_statements(logical_line):
+ r"""
+ Compound statements (multiple statements on the same line) are
+ generally discouraged.
+
+ While sometimes it's okay to put an if/for/while with a small body
+ on the same line, never do this for multi-clause statements. Also
+ avoid folding such long lines!
+
+ Okay: if foo == 'blah':\n do_blah_thing()
+ Okay: do_one()
+ Okay: do_two()
+ Okay: do_three()
+
+ E701: if foo == 'blah': do_blah_thing()
+ E701: for x in lst: total += x
+ E701: while t < 10: t = delay()
+ E701: if foo == 'blah': do_blah_thing()
+ E701: else: do_non_blah_thing()
+ E701: try: something()
+ E701: finally: cleanup()
+ E701: if foo == 'blah': one(); two(); three()
+
+ E702: do_one(); do_two(); do_three()
+ """
+ line = logical_line
+ found = line.find(':')
+ if -1 < found < len(line) - 1:
+ before = line[:found]
+ if (before.count('{') <= before.count('}') and # {'a': 1} (dict)
+ before.count('[') <= before.count(']') and # [1:2] (slice)
+ before.count('(') <= before.count(')') and # (Python 3 annotation)
+ not LAMBDA_REGEX.search(before)): # lambda x: x
+ yield found, "E701 multiple statements on one line (colon)"
+ found = line.find(';')
+ if -1 < found:
+ yield found, "E702 multiple statements on one line (semicolon)"
+
+
+def explicit_line_join(logical_line, tokens):
+ r"""
+ Avoid explicit line join between brackets.
+
+ The preferred way of wrapping long lines is by using Python's implied line
+ continuation inside parentheses, brackets and braces. Long lines can be
+ broken over multiple lines by wrapping expressions in parentheses. These
+ should be used in preference to using a backslash for line continuation.
+
+ E502: aaa = [123, \\n 123]
+ E502: aaa = ("bbb " \\n "ccc")
+
+ Okay: aaa = [123,\n 123]
+ Okay: aaa = ("bbb "\n "ccc")
+ Okay: aaa = "bbb " \\n "ccc"
+ """
+ prev_start = prev_end = parens = 0
+ for token_type, text, start, end, line in tokens:
+ if start[0] != prev_start and parens and backslash:
+ yield backslash, "E502 the backslash is redundant between brackets"
+ if end[0] != prev_end:
+ if line.rstrip('\r\n').endswith('\\'):
+ backslash = (end[0], len(line.splitlines()[-1]) - 1)
+ else:
+ backslash = None
+ prev_start = prev_end = end[0]
+ else:
+ prev_start = start[0]
+ if token_type == tokenize.OP:
+ if text in '([{':
+ parens += 1
+ elif text in ')]}':
+ parens -= 1
+
+
+def comparison_to_singleton(logical_line):
+ """
+ Comparisons to singletons like None should always be done
+ with "is" or "is not", never the equality operators.
+
+ Okay: if arg is not None:
+ E711: if arg != None:
+ E712: if arg == True:
+
+ Also, beware of writing if x when you really mean if x is not None --
+ e.g. when testing whether a variable or argument that defaults to None was
+ set to some other value. The other value might have a type (such as a
+ container) that could be false in a boolean context!
+ """
+ match = COMPARE_SINGLETON_REGEX.search(logical_line)
+ if match:
+ same = (match.group(1) == '==')
+ singleton = match.group(2)
+ msg = "'if cond is %s:'" % (('' if same else 'not ') + singleton)
+ if singleton in ('None',):
+ code = 'E711'
+ else:
+ code = 'E712'
+ nonzero = ((singleton == 'True' and same) or
+ (singleton == 'False' and not same))
+ msg += " or 'if %scond:'" % ('' if nonzero else 'not ')
+ yield match.start(1), ("%s comparison to %s should be %s" %
+ (code, singleton, msg))
+
+
+def comparison_type(logical_line):
+ """
+ Object type comparisons should always use isinstance() instead of
+ comparing types directly.
+
+ Okay: if isinstance(obj, int):
+ E721: if type(obj) is type(1):
+
+ When checking if an object is a string, keep in mind that it might be a
+ unicode string too! In Python 2.3, str and unicode have a common base
+ class, basestring, so you can do:
+
+ Okay: if isinstance(obj, basestring):
+ Okay: if type(a1) is type(b1):
+ """
+ match = COMPARE_TYPE_REGEX.search(logical_line)
+ if match:
+ inst = match.group(3)
+ if inst and isidentifier(inst) and inst not in SINGLETONS:
+ return # Allow comparison for types which are not obvious
+ yield match.start(1), "E721 do not compare types, use 'isinstance()'"
+
+
+def python_3000_has_key(logical_line):
+ r"""
+ The {}.has_key() method will be removed in the future version of
+ Python. Use the 'in' operation instead.
+
+ Okay: if "alph" in d:\n print d["alph"]
+ W601: assert d.has_key('alph')
+ """
+ pos = logical_line.find('.has_key(')
+ if pos > -1:
+ yield pos, "W601 .has_key() is deprecated, use 'in'"
+
+
+def python_3000_raise_comma(logical_line):
+ """
+ When raising an exception, use "raise ValueError('message')"
+ instead of the older form "raise ValueError, 'message'".
+
+ The paren-using form is preferred because when the exception arguments
+ are long or include string formatting, you don't need to use line
+ continuation characters thanks to the containing parentheses. The older
+ form will be removed in Python 3000.
+
+ Okay: raise DummyError("Message")
+ W602: raise DummyError, "Message"
+ """
+ match = RAISE_COMMA_REGEX.match(logical_line)
+ if match and not RERAISE_COMMA_REGEX.match(logical_line):
+ yield match.start(1), "W602 deprecated form of raising exception"
+
+
+def python_3000_not_equal(logical_line):
+ """
+ != can also be written <>, but this is an obsolete usage kept for
+ backwards compatibility only. New code should always use !=.
+ The older syntax is removed in Python 3000.
+
+ Okay: if a != 'no':
+ W603: if a <> 'no':
+ """
+ pos = logical_line.find('<>')
+ if pos > -1:
+ yield pos, "W603 '<>' is deprecated, use '!='"
+
+
+def python_3000_backticks(logical_line):
+ """
+ Backticks are removed in Python 3000.
+ Use repr() instead.
+
+ Okay: val = repr(1 + 2)
+ W604: val = `1 + 2`
+ """
+ pos = logical_line.find('`')
+ if pos > -1:
+ yield pos, "W604 backticks are deprecated, use 'repr()'"
+
+
+##############################################################################
+# Helper functions
+##############################################################################
+
+
+if '' == ''.encode():
+ # Python 2: implicit encoding.
+ def readlines(filename):
+ f = open(filename)
+ try:
+ return f.readlines()
+ finally:
+ f.close()
+
+ isidentifier = re.compile(r'[a-zA-Z_]\w*').match
+ stdin_get_value = sys.stdin.read
+else:
+ # Python 3
+ def readlines(filename):
+ f = open(filename, 'rb')
+ try:
+ coding, lines = tokenize.detect_encoding(f.readline)
+ f = TextIOWrapper(f, coding, line_buffering=True)
+ return [l.decode(coding) for l in lines] + f.readlines()
+ except (LookupError, SyntaxError, UnicodeError):
+ f.close()
+ # Fall back if files are improperly declared
+ f = open(filename, encoding='latin-1')
+ return f.readlines()
+ finally:
+ f.close()
+
+ isidentifier = str.isidentifier
+ stdin_get_value = TextIOWrapper(sys.stdin.buffer, errors='ignore').read
+readlines.__doc__ = " Read the source code."
+
+
+def expand_indent(line):
+ r"""
+ Return the amount of indentation.
+ Tabs are expanded to the next multiple of 8.
+
+ >>> expand_indent(' ')
+ 4
+ >>> expand_indent('\t')
+ 8
+ >>> expand_indent(' \t')
+ 8
+ >>> expand_indent(' \t')
+ 8
+ >>> expand_indent(' \t')
+ 16
+ """
+ if '\t' not in line:
+ return len(line) - len(line.lstrip())
+ result = 0
+ for char in line:
+ if char == '\t':
+ result = result // 8 * 8 + 8
+ elif char == ' ':
+ result += 1
+ else:
+ break
+ return result
+
+
+def mute_string(text):
+ """
+ Replace contents with 'xxx' to prevent syntax matching.
+
+ >>> mute_string('"abc"')
+ '"xxx"'
+ >>> mute_string("'''abc'''")
+ "'''xxx'''"
+ >>> mute_string("r'abc'")
+ "r'xxx'"
+ """
+ # String modifiers (e.g. u or r)
+ start = text.index(text[-1]) + 1
+ end = len(text) - 1
+ # Triple quotes
+ if text[-3:] in ('"""', "'''"):
+ start += 2
+ end -= 2
+ return text[:start] + 'x' * (end - start) + text[end:]
+
+
+def parse_udiff(diff, patterns=None, parent='.'):
+ rv = {}
+ path = nrows = None
+ for line in diff.splitlines():
+ if nrows:
+ if line[:1] != '-':
+ nrows -= 1
+ continue
+ if line[:3] == '@@ ':
+ row, nrows = [int(g) for g in HUNK_REGEX.match(line).groups()]
+ rv[path].update(list(range(row, row + nrows)))
+ elif line[:3] == '+++':
+ path = line[4:].split('\t', 1)[0]
+ if path[:2] == 'b/':
+ path = path[2:]
+ rv[path] = set()
+ return dict([(os.path.join(parent, path), rows)
+ for (path, rows) in list(rv.items())
+ if rows and filename_match(path, patterns)])
+
+
+def filename_match(filename, patterns, default=True):
+ """
+ Check if patterns contains a pattern that matches filename.
+ If patterns is unspecified, this always returns True.
+ """
+ if not patterns:
+ return default
+ return any(fnmatch(filename, pattern) for pattern in patterns)
+
+
+##############################################################################
+# Framework to run all checks
+##############################################################################
+
+
+def find_checks(argument_name):
+ """
+ Find all globally visible functions where the first argument name
+ starts with argument_name.
+ """
+ for name, function in list(globals().items()):
+ if not inspect.isfunction(function):
+ continue
+ args = inspect.getargspec(function)[0]
+ if args and args[0].startswith(argument_name):
+ codes = ERRORCODE_REGEX.findall(function.__doc__ or '')
+ yield name, codes, function, args
+
+
+class Checker:
+ """
+ Load a Python source file, tokenize it, check coding style.
+ """
+
+ def __init__(self, filename, lines=None,
+ options=None, report=None, **kwargs):
+ if options is None:
+ options = StyleGuide(kwargs).options
+ else:
+ assert not kwargs
+ self._io_error = None
+ self._physical_checks = options.physical_checks
+ self._logical_checks = options.logical_checks
+ self.max_line_length = options.max_line_length
+ self.verbose = options.verbose
+ self.filename = filename
+ if filename is None:
+ self.filename = 'stdin'
+ self.lines = lines or []
+ elif lines is None:
+ try:
+ self.lines = readlines(filename)
+ except IOError:
+ exc_type, exc = sys.exc_info()[:2]
+ self._io_error = '%s: %s' % (exc_type.__name__, exc)
+ self.lines = []
+ else:
+ self.lines = lines
+ self.report = report or options.report
+ self.report_error = self.report.error
+
+ def readline(self):
+ """
+ Get the next line from the input buffer.
+ """
+ self.line_number += 1
+ if self.line_number > len(self.lines):
+ return ''
+ return self.lines[self.line_number - 1]
+
+ def readline_check_physical(self):
+ """
+ Check and return the next physical line. This method can be
+ used to feed tokenize.generate_tokens.
+ """
+ line = self.readline()
+ if line:
+ self.check_physical(line)
+ return line
+
+ def run_check(self, check, argument_names):
+ """
+ Run a check plugin.
+ """
+ arguments = []
+ for name in argument_names:
+ arguments.append(getattr(self, name))
+ return check(*arguments)
+
+ def check_physical(self, line):
+ """
+ Run all physical checks on a raw input line.
+ """
+ self.physical_line = line
+ if self.indent_char is None and line[:1] in WHITESPACE:
+ self.indent_char = line[0]
+ for name, check, argument_names in self._physical_checks:
+ result = self.run_check(check, argument_names)
+ if result is not None:
+ offset, text = result
+ self.report_error(self.line_number, offset, text, check)
+
+ def build_tokens_line(self):
+ """
+ Build a logical line from tokens.
+ """
+ self.mapping = []
+ logical = []
+ length = 0
+ previous = None
+ for token in self.tokens:
+ token_type, text = token[0:2]
+ if token_type in SKIP_TOKENS:
+ continue
+ if token_type == tokenize.STRING:
+ text = mute_string(text)
+ if previous:
+ end_row, end = previous[3]
+ start_row, start = token[2]
+ if end_row != start_row: # different row
+ prev_text = self.lines[end_row - 1][end - 1]
+ if prev_text == ',' or (prev_text not in '{[('
+ and text not in '}])'):
+ logical.append(' ')
+ length += 1
+ elif end != start: # different column
+ fill = self.lines[end_row - 1][end:start]
+ logical.append(fill)
+ length += len(fill)
+ self.mapping.append((length, token))
+ logical.append(text)
+ length += len(text)
+ previous = token
+ self.logical_line = ''.join(logical)
+ assert self.logical_line.strip() == self.logical_line
+
+ def check_logical(self):
+ """
+ Build a line from tokens and run all logical checks on it.
+ """
+ self.build_tokens_line()
+ self.report.increment_logical_line()
+ first_line = self.lines[self.mapping[0][1][2][0] - 1]
+ indent = first_line[:self.mapping[0][1][2][1]]
+ self.previous_indent_level = self.indent_level
+ self.indent_level = expand_indent(indent)
+ if self.verbose >= 2:
+ print((self.logical_line[:80].rstrip()))
+ for name, check, argument_names in self._logical_checks:
+ if self.verbose >= 4:
+ print((' ' + name))
+ for result in self.run_check(check, argument_names):
+ offset, text = result
+ if isinstance(offset, tuple):
+ orig_number, orig_offset = offset
+ else:
+ for token_offset, token in self.mapping:
+ if offset >= token_offset:
+ orig_number = token[2][0]
+ orig_offset = (token[2][1] + offset - token_offset)
+ self.report_error(orig_number, orig_offset, text, check)
+ self.previous_logical = self.logical_line
+
+ def generate_tokens(self):
+ if self._io_error:
+ self.report_error(1, 0, 'E902 %s' % self._io_error, readlines)
+ tokengen = tokenize.generate_tokens(self.readline_check_physical)
+ try:
+ for token in tokengen:
+ yield token
+ except (SyntaxError, tokenize.TokenError):
+ exc_type, exc = sys.exc_info()[:2]
+ offset = exc.args[1]
+ if len(offset) > 2:
+ offset = offset[1:3]
+ self.report_error(offset[0], offset[1],
+ 'E901 %s: %s' % (exc_type.__name__, exc.args[0]),
+ self.generate_tokens)
+ generate_tokens.__doc__ = " Check if the syntax is valid."
+
+ def check_all(self, expected=None, line_offset=0):
+ """
+ Run all checks on the input file.
+ """
+ self.report.init_file(self.filename, self.lines, expected, line_offset)
+ self.line_number = 0
+ self.indent_char = None
+ self.indent_level = 0
+ self.previous_logical = ''
+ self.tokens = []
+ self.blank_lines = blank_lines_before_comment = 0
+ parens = 0
+ for token in self.generate_tokens():
+ self.tokens.append(token)
+ token_type, text = token[0:2]
+ if self.verbose >= 3:
+ if token[2][0] == token[3][0]:
+ pos = '[%s:%s]' % (token[2][1] or '', token[3][1])
+ else:
+ pos = 'l.%s' % token[3][0]
+ print(('l.%s\t%s\t%s\t%r' %
+ (token[2][0], pos, tokenize.tok_name[token[0]], text)))
+ if token_type == tokenize.COMMENT or token_type == tokenize.STRING:
+ for sre in re.finditer(r"[:.;,] ?[A-Za-z]", text):
+ pos = sre.span()[0]
+ part = text[:pos]
+ line = token[2][0] + part.count('\n')
+ offset = 0 if part.count('\n') > 0 else token[2][1]
+ col = offset + pos - part.rfind('\n') + 1
+ if sre.group(0)[0] == '.':
+ msg = ('E289 Too many spaces after period. '
+ 'Use only one.')
+ self.report_error(line, col, msg, check=None)
+ elif sre.group(0)[0] == ',':
+ msg = 'E288 Too many spaces after comma. Use only one.'
+ self.report_error(line, col, msg, check=None)
+ else:
+ msg = ('E287 Too many spaces after punctuation. '
+ 'Use only one.')
+ self.report_error(line, col, msg, check=None)
+ if token_type == tokenize.OP:
+ if text in '([{':
+ parens += 1
+ elif text in '}])':
+ parens -= 1
+ elif not parens:
+ if token_type == tokenize.NEWLINE:
+ if self.blank_lines < blank_lines_before_comment:
+ self.blank_lines = blank_lines_before_comment
+ self.check_logical()
+ self.tokens = []
+ self.blank_lines = blank_lines_before_comment = 0
+ elif token_type == tokenize.NL:
+ if len(self.tokens) == 1:
+ # The physical line contains only this token.
+ self.blank_lines += 1
+ self.tokens = []
+ elif token_type == tokenize.COMMENT and len(self.tokens) == 1:
+ if blank_lines_before_comment < self.blank_lines:
+ blank_lines_before_comment = self.blank_lines
+ self.blank_lines = 0
+ if COMMENT_WITH_NL:
+ # The comment also ends a physical line
+ self.tokens = []
+ if self.blank_lines > 1:
+ msg = 'E389 File ends in multiple blank lines'
+ self.report_error(token[2][0], 0, msg, check=None)
+
+ return self.report.get_file_results()
+
+
+class BaseReport:
+ """Collect the results of the checks."""
+ print_filename = False
+
+ def __init__(self, options):
+ self._benchmark_keys = options.benchmark_keys
+ self._ignore_code = options.ignore_code
+ # Results
+ self.elapsed = 0
+ self.total_errors = 0
+ self.counters = dict.fromkeys(self._benchmark_keys, 0)
+ self.messages = {}
+
+ def start(self):
+ """Start the timer."""
+ self._start_time = time.time()
+
+ def stop(self):
+ """Stop the timer."""
+ self.elapsed = time.time() - self._start_time
+
+ def init_file(self, filename, lines, expected, line_offset):
+ """Signal a new file."""
+ self.filename = filename
+ self.lines = lines
+ self.expected = expected or ()
+ self.line_offset = line_offset
+ self.file_errors = 0
+ self.counters['files'] += 1
+ self.counters['physical lines'] += len(lines)
+
+ def increment_logical_line(self):
+ """Signal a new logical line."""
+ self.counters['logical lines'] += 1
+
+ def error(self, line_number, offset, text, check):
+ """Report an error, according to options."""
+ code = text[:4]
+ if self._ignore_code(code):
+ return
+ if code in self.counters:
+ self.counters[code] += 1
+ else:
+ self.counters[code] = 1
+ self.messages[code] = text[5:]
+ # Don't care about expected errors or warnings
+ if code in self.expected:
+ return
+ if self.print_filename and not self.file_errors:
+ print((self.filename))
+ self.file_errors += 1
+ self.total_errors += 1
+ return code
+
+ def get_file_results(self):
+ """Return the count of errors and warnings for this file."""
+ return self.file_errors
+
+ def get_count(self, prefix=''):
+ """Return the total count of errors and warnings."""
+ return sum([self.counters[key]
+ for key in self.messages if key.startswith(prefix)])
+
+ def get_statistics(self, prefix=''):
+ """
+ Get statistics for message codes that start with the prefix.
+
+ prefix='' matches all errors and warnings
+ prefix='E' matches all errors
+ prefix='W' matches all warnings
+ prefix='E4' matches all errors that have to do with imports
+ """
+ return ['%-7s %s %s' % (self.counters[key], key, self.messages[key])
+ for key in sorted(self.messages) if key.startswith(prefix)]
+
+ def print_statistics(self, prefix=''):
+ """Print overall statistics (number of errors and warnings)."""
+ for line in self.get_statistics(prefix):
+ print(line)
+
+ def print_benchmark(self):
+ """Print benchmark numbers."""
+ print(('%-7.2f %s' % (self.elapsed, 'seconds elapsed')))
+ if self.elapsed:
+ for key in self._benchmark_keys:
+ print(('%-7d %s per second (%d total)' %
+ (self.counters[key] / self.elapsed, key,
+ self.counters[key])))
+
+
+class FileReport(BaseReport):
+ print_filename = True
+
+
+class StandardReport(BaseReport):
+ """Collect and print the results of the checks."""
+
+ def __init__(self, options):
+ super().__init__(options)
+ self._fmt = REPORT_FORMAT.get(options.format.lower(),
+ options.format)
+ self._repeat = options.repeat
+ self._show_source = options.show_source
+ self._show_pep8 = options.show_pep8
+
+ def error(self, line_number, offset, text, check):
+ """
+ Report an error, according to options.
+ """
+ code = super().error(line_number, offset,
+ text, check)
+ if code and (self.counters[code] == 1 or self._repeat):
+ print((self._fmt % {
+ 'path': self.filename,
+ 'row': self.line_offset + line_number, 'col': offset + 1,
+ 'code': code, 'text': text[5:],
+ }))
+ if self._show_source:
+ if line_number > len(self.lines):
+ line = ''
+ else:
+ line = self.lines[line_number - 1]
+ print((line.rstrip()))
+ print((' ' * offset + '^'))
+ if self._show_pep8 and check is not None:
+ print((check.__doc__.lstrip('\n').rstrip()))
+ return code
+
+
+class DiffReport(StandardReport):
+ """Collect and print the results for the changed lines only."""
+
+ def __init__(self, options):
+ super().__init__(options)
+ self._selected = options.selected_lines
+
+ def error(self, line_number, offset, text, check):
+ if line_number not in self._selected[self.filename]:
+ return
+ return super().error(line_number, offset, text, check)
+
+
+class TestReport(StandardReport):
+ """Collect the results for the tests."""
+
+ def __init__(self, options):
+ options.benchmark_keys += ['test cases', 'failed tests']
+ super().__init__(options)
+ self._verbose = options.verbose
+
+ def get_file_results(self):
+ # Check if the expected errors were found
+ label = '%s:%s:1' % (self.filename, self.line_offset)
+ codes = sorted(self.expected)
+ for code in codes:
+ if not self.counters.get(code):
+ self.file_errors += 1
+ self.total_errors += 1
+ print(('%s: error %s not found' % (label, code)))
+ if self._verbose and not self.file_errors:
+ print(('%s: passed (%s)' %
+ (label, ' '.join(codes) or 'Okay')))
+ self.counters['test cases'] += 1
+ if self.file_errors:
+ self.counters['failed tests'] += 1
+ # Reset counters
+ for key in set(self.counters) - set(self._benchmark_keys):
+ del self.counters[key]
+ self.messages = {}
+ return self.file_errors
+
+ def print_results(self):
+ results = ("%(physical lines)d lines tested: %(files)d files, "
+ "%(test cases)d test cases%%s." % self.counters)
+ if self.total_errors:
+ print((results % ", %s failures" % self.total_errors))
+ else:
+ print((results % ""))
+ print(("Test failed." if self.total_errors else "Test passed."))
+
+
+class StyleGuide:
+ """Initialize a PEP-8 instance with few options."""
+
+ def __init__(self, *args, **kwargs):
+ # build options from the command line
+ parse_argv = kwargs.pop('parse_argv', False)
+ config_file = kwargs.pop('config_file', None)
+ options, self.paths = process_options(parse_argv=parse_argv,
+ config_file=config_file)
+ if args or kwargs:
+ # build options from dict
+ options_dict = dict(*args, **kwargs)
+ options.__dict__.update(options_dict)
+ if 'paths' in options_dict:
+ self.paths = options_dict['paths']
+
+ self.runner = self.input_file
+ self.options = options
+
+ if not options.reporter:
+ options.reporter = BaseReport if options.quiet else StandardReport
+
+ for index, value in enumerate(options.exclude):
+ options.exclude[index] = value.rstrip('/')
+ # Ignore all checks which are not explicitly selected
+ options.select = tuple(options.select or ())
+ options.ignore = tuple(options.ignore or options.select and ('',))
+ options.benchmark_keys = BENCHMARK_KEYS[:]
+ options.ignore_code = self.ignore_code
+ options.physical_checks = self.get_checks('physical_line')
+ options.logical_checks = self.get_checks('logical_line')
+ self.init_report()
+
+ def init_report(self, reporter=None):
+ """Initialize the report instance."""
+ self.options.report = (reporter or self.options.reporter)(self.options)
+ return self.options.report
+
+ def check_files(self, paths=None):
+ """Run all checks on the paths."""
+ if paths is None:
+ paths = self.paths
+ report = self.options.report
+ runner = self.runner
+ report.start()
+ for path in paths:
+ if os.path.isdir(path):
+ self.input_dir(path)
+ elif not self.excluded(path):
+ runner(path)
+ report.stop()
+ return report
+
+ def input_file(self, filename, lines=None, expected=None, line_offset=0):
+ """Run all checks on a Python source file."""
+ if self.options.verbose:
+ print(('checking %s' % filename))
+ fchecker = Checker(filename, lines=lines, options=self.options)
+ return fchecker.check_all(expected=expected, line_offset=line_offset)
+
+ def input_dir(self, dirname):
+ """Check all files in this directory and all subdirectories."""
+ dirname = dirname.rstrip('/')
+ if self.excluded(dirname):
+ return 0
+ counters = self.options.report.counters
+ verbose = self.options.verbose
+ filepatterns = self.options.filename
+ runner = self.runner
+ for root, dirs, files in os.walk(dirname):
+ if verbose:
+ print(('directory ' + root))
+ counters['directories'] += 1
+ for subdir in sorted(dirs):
+ if self.excluded(subdir):
+ dirs.remove(subdir)
+ for filename in sorted(files):
+ # contain a pattern that matches?
+ if ((filename_match(filename, filepatterns) and
+ not self.excluded(filename))):
+ runner(os.path.join(root, filename))
+
+ def excluded(self, filename):
+ """
+ Check if options.exclude contains a pattern that matches filename.
+ """
+ basename = os.path.basename(filename)
+ return filename_match(basename, self.options.exclude, default=False)
+
+ def ignore_code(self, code):
+ """
+ Check if the error code should be ignored.
+
+ If 'options.select' contains a prefix of the error code,
+ return False. Else, if 'options.ignore' contains a prefix of
+ the error code, return True.
+ """
+ return (code.startswith(self.options.ignore) and
+ not code.startswith(self.options.select))
+
+ def get_checks(self, argument_name):
+ """
+ Find all globally visible functions where the first argument name
+ starts with argument_name and which contain selected tests.
+ """
+ checks = []
+ for name, codes, function, args in find_checks(argument_name):
+ if any(not (code and self.ignore_code(code)) for code in codes):
+ checks.append((name, function, args))
+ return sorted(checks)
+
+
+def init_tests(pep8style):
+ """
+ Initialize testing framework.
+
+ A test file can provide many tests. Each test starts with a
+ declaration. This declaration is a single line starting with '#:'.
+ It declares codes of expected failures, separated by spaces or 'Okay'
+ if no failure is expected.
+ If the file does not contain such declaration, it should pass all
+ tests. If the declaration is empty, following lines are not checked,
+ until next declaration.
+
+ Examples:
+
+ * Only E224 and W701 are expected: #: E224 W701
+ * Following example is conform: #: Okay
+ * Don't check these lines: #:
+ """
+ report = pep8style.init_report(TestReport)
+ runner = pep8style.input_file
+
+ def run_tests(filename):
+ """Run all the tests from a file."""
+ lines = readlines(filename) + ['#:\n']
+ line_offset = 0
+ codes = ['Okay']
+ testcase = []
+ count_files = report.counters['files']
+ for index, line in enumerate(lines):
+ if not line.startswith('#:'):
+ if codes:
+ # Collect the lines of the test case
+ testcase.append(line)
+ continue
+ if codes and index:
+ codes = [c for c in codes if c != 'Okay']
+ # Run the checker
+ runner(filename, testcase, expected=codes,
+ line_offset=line_offset)
+ # output the real line numbers
+ line_offset = index + 1
+ # configure the expected errors
+ codes = line.split()[1:]
+ # empty the test case buffer
+ del testcase[:]
+ report.counters['files'] = count_files + 1
+ return report.counters['failed tests']
+
+ pep8style.runner = run_tests
+
+
+def selftest(options):
+ """
+ Test all check functions with test cases in docstrings.
+ """
+ count_failed = count_all = 0
+ report = BaseReport(options)
+ counters = report.counters
+ checks = options.physical_checks + options.logical_checks
+ for name, check, argument_names in checks:
+ for line in check.__doc__.splitlines():
+ line = line.lstrip()
+ match = SELFTEST_REGEX.match(line)
+ if match is None:
+ continue
+ code, source = match.groups()
+ checker = Checker(None, options=options, report=report)
+ for part in source.split(r'\n'):
+ part = part.replace(r'\t', '\t')
+ part = part.replace(r'\s', ' ')
+ checker.lines.append(part + '\n')
+ checker.check_all()
+ error = None
+ if code == 'Okay':
+ if len(counters) > len(options.benchmark_keys):
+ codes = [key for key in counters
+ if key not in options.benchmark_keys]
+ error = "incorrectly found %s" % ', '.join(codes)
+ elif not counters.get(code):
+ error = "failed to find %s" % code
+ # Keep showing errors for multiple tests
+ for key in set(counters) - set(options.benchmark_keys):
+ del counters[key]
+ report.messages = {}
+ count_all += 1
+ if not error:
+ if options.verbose:
+ print(("%s: %s" % (code, source)))
+ else:
+ count_failed += 1
+ print(("%s: %s:" % (__file__, error)))
+ for line in checker.lines:
+ print((line.rstrip()))
+ return count_failed, count_all
+
+
+def read_config(options, args, arglist, parser):
+ """Read both user configuration and local configuration."""
+ config = RawConfigParser()
+
+ user_conf = options.config
+ if user_conf and os.path.isfile(user_conf):
+ if options.verbose:
+ print(('user configuration: %s' % user_conf))
+ config.read(user_conf)
+
+ parent = tail = args and os.path.abspath(os.path.commonprefix(args))
+ while tail:
+ local_conf = os.path.join(parent, '.pep8')
+ if os.path.isfile(local_conf):
+ if options.verbose:
+ print(('local configuration: %s' % local_conf))
+ config.read(local_conf)
+ break
+ parent, tail = os.path.split(parent)
+
+ if config.has_section('pep8'):
+ option_list = dict([(o.dest, o.type or o.action)
+ for o in parser.option_list])
+
+ # First, read the default values
+ new_options, _ = parser.parse_args([])
+
+ # Second, parse the configuration
+ for opt in config.options('pep8'):
+ if options.verbose > 1:
+ print((' %s = %s' % (opt, config.get('pep8', opt))))
+ if opt.replace('_', '-') not in parser.config_options:
+ print(('Unknown option: \'%s\'\n not in [%s]' %
+ (opt, ' '.join(parser.config_options))))
+ sys.exit(1)
+ normalized_opt = opt.replace('-', '_')
+ opt_type = option_list[normalized_opt]
+ if opt_type in ('int', 'count'):
+ value = config.getint('pep8', opt)
+ elif opt_type == 'string':
+ value = config.get('pep8', opt)
+ else:
+ assert opt_type in ('store_true', 'store_false')
+ value = config.getboolean('pep8', opt)
+ setattr(new_options, normalized_opt, value)
+
+ # Third, overwrite with the command-line options
+ options, _ = parser.parse_args(arglist, values=new_options)
+
+ return options
+
+
+def process_options(arglist=None, parse_argv=False, config_file=None):
+ """Process options passed either via arglist or via command line args."""
+ if not arglist and not parse_argv:
+ # Don't read the command line if the module is used as a library.
+ arglist = []
+ if config_file is True:
+ config_file = DEFAULT_CONFIG
+ parser = OptionParser(version=__version__,
+ usage="%prog [options] input ...")
+ parser.config_options = [
+ 'exclude', 'filename', 'select', 'ignore', 'max-line-length', 'count',
+ 'format', 'quiet', 'show-pep8', 'show-source', 'statistics', 'verbose']
+ parser.add_option('-v', '--verbose', default=0, action='count',
+ help="print status messages, or debug with -vv")
+ parser.add_option('-q', '--quiet', default=0, action='count',
+ help="report only file names, or nothing with -qq")
+ parser.add_option('-r', '--repeat', default=True, action='store_true',
+ help="(obsolete) show all occurrences of the same error")
+ parser.add_option('--first', action='store_false', dest='repeat',
+ help="show first occurrence of each error")
+ parser.add_option('--exclude', metavar='patterns', default=DEFAULT_EXCLUDE,
+ help="exclude files or directories which match these "
+ "comma separated patterns (default: %default)")
+ parser.add_option('--filename', metavar='patterns', default='*.py',
+ help="when parsing directories, only check filenames "
+ "matching these comma separated patterns "
+ "(default: %default)")
+ parser.add_option('--select', metavar='errors', default='',
+ help="select errors and warnings (e.g. E,W6)")
+ parser.add_option('--ignore', metavar='errors', default='',
+ help="skip errors and warnings (e.g. E4,W)")
+ parser.add_option('--show-source', action='store_true',
+ help="show source code for each error")
+ parser.add_option('--show-pep8', action='store_true',
+ help="show text of PEP 8 for each error "
+ "(implies --first)")
+ parser.add_option('--statistics', action='store_true',
+ help="count errors and warnings")
+ parser.add_option('--count', action='store_true',
+ help="print total number of errors and warnings "
+ "to standard error and set exit code to 1 if "
+ "total is not null")
+ parser.add_option('--max-line-length', type='int', metavar='n',
+ default=MAX_LINE_LENGTH,
+ help="set maximum allowed line length "
+ "(default: %default)")
+ parser.add_option('--format', metavar='format', default='default',
+ help="set the error format [default|pylint|<custom>]")
+ parser.add_option('--diff', action='store_true',
+ help="report only lines changed according to the "
+ "unified diff received on STDIN")
+ group = parser.add_option_group("Testing Options")
+ group.add_option('--testsuite', metavar='dir',
+ help="run regression tests from dir")
+ group.add_option('--doctest', action='store_true',
+ help="run doctest on myself")
+ group.add_option('--benchmark', action='store_true',
+ help="measure processing speed")
+ group = parser.add_option_group("Configuration", description=(
+ "The project options are read from the [pep8] section of the .pep8 "
+ "file located in any parent folder of the path(s) being processed. "
+ "Allowed options are: %s." % ', '.join(parser.config_options)))
+ group.add_option('--config', metavar='path', default=config_file,
+ help="config file location (default: %default)")
+
+ options, args = parser.parse_args(arglist)
+ options.reporter = None
+
+ if options.testsuite:
+ args.append(options.testsuite)
+ elif not options.doctest:
+ if parse_argv and not args:
+ if os.path.exists('.pep8') or options.diff:
+ args = ['.']
+ else:
+ parser.error('input not specified')
+ options = read_config(options, args, arglist, parser)
+ options.reporter = parse_argv and options.quiet == 1 and FileReport
+
+ if options.filename:
+ options.filename = options.filename.split(',')
+ options.exclude = options.exclude.split(',')
+ if options.select:
+ options.select = options.select.split(',')
+ if options.ignore:
+ options.ignore = options.ignore.split(',')
+ elif not (options.select or
+ options.testsuite or options.doctest) and DEFAULT_IGNORE:
+ # The default choice: ignore controversial checks
+ # (for doctest and testsuite, all checks are required)
+ options.ignore = DEFAULT_IGNORE.split(',')
+
+ if options.diff:
+ options.reporter = DiffReport
+ stdin = stdin_get_value()
+ options.selected_lines = parse_udiff(stdin, options.filename, args[0])
+ args = sorted(options.selected_lines)
+
+ return options, args
+
+
+def _main():
+ """Parse options and run checks on Python source."""
+ pep8style = StyleGuide(parse_argv=True, config_file=True)
+ options = pep8style.options
+ if options.doctest:
+ import doctest
+ fail_d, done_d = doctest.testmod(report=False, verbose=options.verbose)
+ fail_s, done_s = selftest(options)
+ count_failed = fail_s + fail_d
+ if not options.quiet:
+ count_passed = done_d + done_s - count_failed
+ print(("%d passed and %d failed." % (count_passed, count_failed)))
+ print(("Test failed." if count_failed else "Test passed."))
+ if count_failed:
+ sys.exit(1)
+ if options.testsuite:
+ init_tests(pep8style)
+ report = pep8style.check_files()
+ if options.statistics:
+ report.print_statistics()
+ if options.benchmark:
+ report.print_benchmark()
+ if options.testsuite and not options.quiet:
+ report.print_results()
+ if report.total_errors:
+ if options.count:
+ sys.stderr.write(str(report.total_errors) + '\n')
+ sys.exit(1)
+
+
+if __name__ == '__main__':
+ _main()
diff --git a/sbapp/plyer/tools/pep8checker/pep8base.html b/sbapp/plyer/tools/pep8checker/pep8base.html
new file mode 100644
index 00000000..e69ca6fb
--- /dev/null
+++ b/sbapp/plyer/tools/pep8checker/pep8base.html
@@ -0,0 +1,70 @@
+<html>
+ <head>
+ <title>Kivy Styleguide Check</title>
+ <style type="text/css">
+ body, html
+ {
+ background: black url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQMAAAGQCAMAAACd/fUUAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAAxQTFRFJysvIyMjJSgqJCYnQLTKlAAAMgtJREFUeNrMXYty5DgOk4H//+ebbtsS36Ld2apL7d3OThK3DVMUCYLUGJ8vHgfG+joOqv/62y/8xbXnRZj+COdn/Puh+VGQnywfE//+e/2XQYT1fXx+l/j3df0c/33V98+/wJcdDPi9P45//+D6W/3JQ78PymceBqDqVcg/sfdoe3zbpoACAxynGYx/P8sQA9jnxPzTeIIBj2H/1Dfl+l3j83PTwh5igO/94N+/sZYCDfrpgyJHgOeXwoDrZ9Mno1wu4rfR8xuMvw+WxvZdDuf/YC8Kj4G4HMPPZmw24/yk0g4YY4qtlX1Q4xe9pudQnui7BMbnOsFSiBbDCcPnA0NYzI/qlSUMO3i5HPZjNDgWhAkwrWHvAPhn9PKl/buf+778Uph/M6ovhE5MWMewdoAAA0aX1mvEPcv3keE83BYAHoe4tngS+KUQOoSRrIwABNoVvf7ePiPiy2f2z/lrfokAOwC0uayP5vpAfxPomICHVb1isY8cDoMU49QJnM8S/kC2GgjpyxBZIOQN3Y9dGELsa6O4RkZD158JGSEVKy3dDD5vkuH3Y0Pg936xfF7kiylsg2bFNgFQEPTDmra3iaKbyEguZ2E2gvNR52MzDJeFGcw7Y7AYwMZWjVd5QQuEFW6v7+L+5/SK1LcJE///g8hcGOq1zc8VILADQLyy2gF99sXCt4kF/LnfrwV8MdCB1pCO78IAwyGrXATlzQk8/PN/7Us9DM9/H39kBuJSOkjAitg/r/mMuxDujhy0L4flHuoe6nLoUZKn41heMFNcgfzJG/gtFjpmGOtbWIlumE6YrY6Fw/J7VnLdf1bhQynpcyFCInxumHxjBiaFZ7QPDvUfOIoYvIEBNdYT3BHcOM3in66qWubXOmXbDK7HRuhorjV8XvN2Y/Qr3ZkVN3xH9M0RmO9R7OLsmngDA+QY3O8eE9cgbeIIkrEisRgPMLhszOTtc3G6h2YAAn/E4DJpiE/3jnswwqDcqI4uBv8AhvV8gfGbCFlZT8Md6PQzeEBxewy35eH2dhS7N3SSgAoDCJdbePnFO+EnDEwYIy2dnKsckZFPhoHXbhJFOd8fuR8B0VZkMYB2t8i9/NprokcbfQx8BGx9GC/C073ic1/68AaId6e1JkXcYbeiYbxxyJZEj4SFWbXMexhoUkU6AdS+HsgePnbh4VY0VuQd3jr3GCBKBN5gwBHleT/xz+EnJhiY77oQNMSAMhM194/uvmA3Hshn5o91CD7AQKdTwbsKHwCquLBFbZM1QVGS5j2+tIP43uxWNCQER+277TfkPb+JkaLbPTd4DNgaxsZ/9pYC44z6dER5gJsu74sURvJkeIjBd4s/n58r2vMYYJu+I+eIjhDKL/e+xyA155Q557OlgIuc5XHV7BBnVNhyGMhvO/nFcSXsZ3SRva30pSJ/uY/N4HKyV5k0XroM9xNmtVi0SMyhokgktzq6X+gTCPQYLMu3DuHw1EdCZSinzx1ztTDgSPdGX4YW91k/F59sjBIDikSZMi8GDhPC0wXHSNdwuoSGhCC8bWb7hfWI6e104iPhX6kS5UXgc1Z1qYsyzDFItSQ0GIyoHKcwYMMj8hGXxDA34CRCkMQ7FLnThQDC/TCNPVc6cL+0ISHIAwQ+3Ol2y4GqtGZ2bIZXcwYNxvGC3vxpfi8oJ4xjVx9sbnV4wi7Tb/XzliSlnb/f6/VnJSFDvUKWr/mlh24i7rsjg+WLU5cCnteNUlj1Xl8XMT4Zrf5dTTN1Y2co5GlLsiEI6FHmpR7E5uP2FtwyTQoDiCH4ltt6ATSGXWfMvVPxZMHzd27glJNoOY5dqszIgOvH1kYoIDhLrl9GaXcnH6YledzoftMiCi2d+NQco2v1tHzUpLN0CGMqaDZ8SmJjz55CujUC+EVcdzSL2a56TMOAcVWdx9GrMmnzeJScPyZ2WEk2+7V4xW3CVp3wKY2BknP8fq4KoZgmDGZv7Jgv9kJURhsISoFPiapyh8OVEc5gTVTkZtrJJWlzhiQwuPHhxrmTDQqD3PAGCoi+LEOqquA4ZonBSbirt+bMwHAQY5ZwaO9QgsKW70IvuWQWOvQMcZkBqXQI31rDBQ+GwQCS2tCveyyE1quCL7FgS+GgnWCnGJi8M1QvDiHSmCtd+TtQhppYGeStUIChHa6cieFDiKD7a+Vfy8j2Am/0p8a0iClRpVKlTNeoQA8VZnzRgwbtWAG6C6mHMBOkN8SpSL1i5hIDG6sws3kW+JQrxFx/HFMEwXHL9Q9VNRC88RVarPBySDECMhflMtBgZ6iCyxXlcIOBEr80ozCsD+WZdcG5xEPpfe6kK8BARIHcVIA8CuUOR0VvVckH/TIx4UYRu9qgSZGzBoPvDgk6DNInMT0uV2ne/8ix1z9Aa+WS5CtcOkV4bsDBOIwkwxfbv5wdlk8cCQPhvJeKFhi+6QcYIN0aQwaDRmZbhuFU7kBh8C3pf98jPJ84HyYDgalvYn1vkHJ37DFAVOWhlIdvQdAhtX5GSPnUsBhldQz5jAxA2KgXp3mlxEnaLTBDnIEnRdhrWxbltWx/iTBI3qnd8fKQPt8ZrBixCJGsvKnZbOdYk0MsJftdFR8kHG7MR3GX1mzZVWvyyEEQwjy80gwf0WJQqdMIzKBiFljx6HiMAU1Pqec2xS4/fsIAqTcbsewte1hegQ7+CIPDytMQ+lHa3ts39FXBHzC7YFwSQxCw/4ABrGDB9EKJaA8/YoAMhIpDQbRjUSSSrzGgX5TRctAYpHuITMwqei+7w8rHMkidZs46+DcYuJAAkfWK/tERM/Idsj/WgpbbTCQpOG4pllW8/46BrbObQmgg0BbMNFo4RGGevBys0BGBUBOL38R4ggE63OFQ6sRJbKQYzj6eu6Fl/sVVEXMLI958fPafZvBSguVcAhpxYswkqYfOFzCiWt0lwIVWtfEQ6ohAeT6g7cjD7LNXmewITfNgH4NCH1aII0ziDPVMuBusCbGJ3W47WxX0ybrS3jERlklGTsXwzzFQnRS7OQ7lJbnaNe8Vylls+YQwd2EZxthcC/pQyT8KDCjo58B067xRkDOBqWGbcGdxn9YFnevzq/C7hc7LjWvKHIKCzzE4vNLZPxka/EFQc3iAQbVQrirZuF36yk+/MzAEI4SqOj5UTBgznjh8cslrb3B6D93+mwgWjt8xuB72NGauqgHOh/2WqWX7Y8HxDBUo6J+j4TCCpYoHesQgVnbkFNUsgFJU+AkY7urBvRF+DePjHb606pxKAMwQixsMgED8R5UpKA2tdglvMDgQKPXZxOBzc9bEcaehV1OsF1CzxqBy0hzDZnnQiUO/zmTmZDh2Xi15pE4WYhrNqe+YgSbH9ea/FTOwrtn4wgJzeg6rNVREFvwNg6PEYBUJr8EoVAaEkXZWfGC4BxJ8AmDQxeKfK5JTk8Up1CrMQKwAz+WzjwHrGOKavcNK03b9oNGJXcE+hpM2CDVlyOjiQorc1L4hequXnwSeOoS4PoAHRsT4aXiFQKTvN4H9eENpYKcwW/GRFWSsNBo/LIWnDhUioLm2RPVWRdP2lVGQ82fC3i1d0QgKblwzFo41gGQZWrgYcH9J045FLHyKwRiuiUZpiukShq8HJUbW4KH1kWEwgyBLXo18rPi3RjT9GoNrkhQ08Euud++fZ+QIcIcBVkDvsmYxtEj6maGI+k3W8HcYcOUJtzvRngamsHvNKRjI+tRmJ4vUHZpCuAwNVwYBt8lXvfd2FSypxEs7wOH6K3HG57c143r138YALfKjEt7PJDPDANPsAsLoFjS84bo3vQE7n3hNxaPdBuYmg8knfSxAB/+THP1ax+0p6DqUIAKkGR/d5UrZaf4cAyVpeIzBSpqxdolbgs1LX7YynesHlE88N7mPgGURkndR47IgqgoHo8IsRMSIl1YwRtg2vt9Zl+YGOjw8TloJizQQCbDwV1QbwFzit3+cUxyi+RpUm8P892sziPqFGxhcRkuuQu7gtA1c7o3nvyH0xWo8EEyIBBkr4FBNrq72I+i7c894UfQxbcMPA+3ZfIC7OAoVNZw5k974lNBZA8s0XhIYnGO4ZJeXLEMj0MbcyRzvYFgESAiSnL0JKPJ/vkiuYRQiKpabPMTWT4QYHHWIBNPkHuyBqIJfBqVC7jvntNJxT1afzltNsNBtd1+aEUGJ6tb1TdlGtDWubSEqOTOuwEr22ltz3UWKXRIXfsxhe3DtD3xjBGOtDCZrMahoIRlMQ6DR8cMjzfxWuD1D21du5bvD3ZN0V+KjA4evhp3IONXo0dbbHV6Kt9hSYF9drNoFX1TRGRYpp8F/R+l+Y+Ornf5QCWQLgwhtqEx/ZYOdyprzeBJHdqLq+NK6+kTdg3ut2kW436zL8Q4DWbzjwwCowCDTkxXmgZ72ezZbeTE632JwcDfQ+REGbHk5bqmWzPCybZfxPNI/n6ofyQ6bGFg2jTu2KZg0YPpQcYf0CEUT/38Y7Ei3YuDEufrDQSR3ywWzsXn/HQY3g8YgAmLDkYr4KGdcqZQLHNFI37V9s6y1/TkIGYlybqnsmUGRNNG3DR9XaABk6wbjeIQB0PaBl4onfODFqD7KorZJE31HzMrjPsyGn1Uz6nkoV4FlscBP+njPBzTN40xznsZeUrTXD9N7JZJI0n1XZwupyY+yIw9PfACb77Qe5I3OBWDV35RcwKMh7yZT/gUE0hftNxSAbpc2pldkkSpBEgU/3I/P3ZOI8z06K7DnDkpFfnY/iB56Z0mivKXHh1xFv1PVi6JzMrV7sIPBNd5Qdw7dZU6MfrUgkGR6R5/9JuTeAIEBefn+UQxRr84a2GJAxN2fcwDu6Ht4jwGbGGBSg4SGENe8EK4xSu6FFa/naGCAm24hjMTwXtXob/SpQBIdW4Iki5RkRZFh4IN6FvLvQheeA4GrD4zac4TkUT3wG351u27WvOIBgUKhvg86kkx/jIJRuPQcDwQprjfMz7bsxQeqWIfd4VfoFa9ibyot1ukzuYq7jzBQwknlvzZyDJ08qOw+R4Ako/B56w5gnjfoqb+nzq3Db7o1Aoy0EP5K41b0weZv2Oh1Cgd+eAzWbAVeFUg+xEA9QxIPYr9muWXhsPcBqLdj2bKi9ZXXdrD8WruO3sHAXg0NIuVZPkodl6B0FaGQHJpnDXV+P6wFeMV7smO8heAJBlYwCN6RtjU1PsKAPkxMpayjGBjwOiWnLOKnYK1WDbhGlBuDK8r8CD7aU8b96v0BAxqZlFb9jC4GLLfiI5xgIwbO3AVddj28zxvzVb6NnugyMPpKTrrryXdau4ORY2ADxcdSktoO6GOVUZTuKNWLG6vUGNSu68oKsBSGPNzMUz7YGofPFIvwLK1XxwKlQ/UbtRVurNP8WF0Ha218hUFqsu0jTRwG6GIA0S+AKkiE2RfEC5Kvc67tv8Ig5cTKBMJigM19zPC7TBlpXDIPJW1zQ57bkqI4FHrxBYNom5hctFvM4kK3LqymikAJ+g4DNVfwZwwWtcbxN4mI6YoM2Qg8DkqK8PYXDIaXzI1Enf4OgSM+8CePz9hbwH0MlpIr63EIX8JvFhY9cDA05nmAXkS37XshdtcbzzHw7iCWF7laAF4bQhcDNIuz4aU6r4RTuIr9cuXcAIFtssLn7uBoLpnYFsJP79zMkZ4xRVG8KR/sJQi9BdSYMlth0CAmZ4yEXNkAN9hyjuzc3SYemsHxDIKjGC4YHMD5/EsNaKhHTeN4YQqG6YxBQ1TJT+ol1bbAH3eFahReNKhc3ybr8i91y2UlRWI6ICsauTp+x4Duck0QonY4pAiPqEgS3gSau8WLbaGzVstpheabl6tAUOdewoCoNB2KvBuseBODZ4aAdEZqWbWhiSXZ4XpLDNhwrbuJG3iu8UnuGyVCUDS9mgbRmAM7sn2crVeILgZPQNjGvSjMDvegVdb7SY6Bn0j14KBwoSALak38wQqO7THnItqSEXTDEHROtP7qiQ0jit0FKQKngUbVWg6rNVH5YS6uW7SFJICLGDKZkXJ9NJ9ZMALq+cYgP1oCLlvS2kI/TNpvVGbs6Sy83bQM4il96YCYEDE8sV5BpM1sIR/mYGqYDIaemaM0h5eHx4uCd/2fHoPtbIO3gk1YWheaJIsG5ftCmv9kHRsxcNTJFiEbv9B//gADPgzp1nXsdZnmlYKK9OCbozSHF8zFB4lhHRCxJIivNqXsaDTk6oCEig7ifR94D0MX+aM06X1PfLoNBASVxAlbDHbVgLTK5K/rXaN3J8PShu4ozWO0ZnvlZY8oFC1P4ECKAbeGYL95/SZdMi12Oju1MjhKky6g3VEGCbWPGrP4W6tZK1FRNupREIPgArduMQiO0jxGdiCurq8wKIaHRZvgSfNEUW//dXUhx0BmqO4IjQQDeZSmnUlv3T1gdPyxZ5P3yiJTDCxkXSH3p3U2WOZZOhKIj9IMuHU0Im2GvuucrGDVGvmgS245gPZkIlaKc0md+aM0/Vm6aF3f854cU8xgQMjTPGyfj8/UsdGrPGALdtr8EVJsjaTLBSPEdXaeLRYPd5hl7BDKgPldIAZwJn0rcTNHaUZsc9PSXH3ccictDLh7xe3CYpAWzOIxA3NCluc/yHus5ENv8Ec0LjiysrgsDjv++dFpqzN8/CSMZzQDsz6iLH7rerDBALBTD3oYZEbH0a+mxIT5JbAdeod0R2liG4ss5LjBQL/RBINehiFbR7sYOLKPlPEB9kOBU3kPQ/7KPs4RHb4+XmMwhKCqO8hTfEQUI7FRNctlQ1kMjeiORxz58ikG1AHXfhUhbkNcq2A/Kb3S4WKPAQPyApmh9sgTx2a0IyRjsRaD4IxWMfvgWSCGJOYN/vMpBuiIeNNtyxjC16WJKSG0NfcnEhMWQRJd8h3PtOB+XzQtFz1XojPFIIU1DgH8ewxmt8PN4WbnU6VuM+FNmmy8ubOo+wWqPIj/AIPB6ptJJZL72LjHQtp02W8mmCNRTzN4hQFo6feioJnrDeA9f3SRaLRQP1e4zoPVHut2evVZ8vGJxfQnmCC5J9UmWgRxq+6SwbhnHuoY1qchZtyXn/3VUoRZ+v1dxXddWwOG0I56VYngPv2EEmp9SABbr48Gked+hgEXlY5t5NbrZo5EofRMxZjNNRwIop2U4PgZAzd3aq8zQObuSzO4Z6DN0NiR0KcjuFoVmWDAP8UgmbrFjTIEqR32mCYmhnCUDmEkHRqopTD5w9czx1DoY1idEYAW0XTzy/aN3mEDkBR+0PMHipIL95Cj8RVRJkHVHj1FYMyEHPBzHWffMvxYxTRvTBLu49evt7qRwiG6v3CGMFMGHt4fUGQ5iMb3vX5oNUTpXSfITteuqDZ99ok1hInB3cJtMWB0ILgo1j6fgZkHgR0eASW08mQSWdzRXIV1vyOTn56RpdYV2MnlKNz4My0vWpQI+G51aQxIdfIE7uHj4eE9OK7BzUr6OAehDGmFfIoAzMEj2JlCfKR8bYUUEga1/c17KIaJHXcVXlkW1YnlOuF5NhotIk/K/ITFqtqOILVPeh5txkLBpSLl71xvKM3OZGU4K6+mPht6CcSLGnElAGG+4DRUYIn/pUihOU7lmLX93eoSb5wecw5VkoQ+teGefrOifz/MBMgMoWNSStiRmvKlFLMYbI4nP+Io1Qm5YVQ/87gOe15gUs2i5woebK7m7NNs0O81QCrsSSwJ5RgWx1zAHEFyTqjxg0AZMq0MKFa+wwDnqQtB+ZfXOSQPMLgGKiabjFk6U7UwI+H5hqMzel3SPvy0Fz7Z83IlzHpxc8xeE4M1BJWhddB7zEn5k2fd2i/PSjDCvTy8gQFTTQcVBtxiQKZPS04piZLc0B2wR7fbcDf9ZYy3dmDYJsIVHXBvnziicZrjpo7gWGe4rhaoB6TcGs3sMNcMhXQ+QpRi0nKTcrSa3m7N6FEY7RCn7LTAAJqWGms7QHgAi3gQte2AVt2A+Fz3UJpYZrvyT7QbB5Lqi7pTzBjWezmu3ofvHynPJ6Uj+eBEHL5wxXvWtQvXi8ICSgwoSOStKCbEYASKQ82RqekekJok43FXpd+gs2ghIvYm4dnZTXWNOEnKnn4aZhNIC7Ha0V+uG0N2H5mA0zWHIVolTKWWyQyFtnBIHCaR2EFQv0gLsacUW9/HhcExdzWaN3k4uBEVbjldFvNdFGabNDxZfBK6Fl7f79HwIurgBbi5rJ+7WMdPyfeKxY2JGU/2KGhHJofzQaptC+Eeg1pl6ielMXlEzCB75VVZ6e47el74OBmbHTKSp7pdV+6C2XQbLEGIWYmBu33I/l27FO77RC68mVeBEWHP4Gx5AEJ4dq4sE8nttSxhpkUZhYI8m6atVBxbDEAi0bldFQVVsuUd/E8MJCdxctmy/Obz+k5KE7c8+Nay8MdkNMT7jF6qQXOX/NmFWyEG7oQG3OtUDcGsmah33HFw9Az+hCVESkRGZWwO4Q60EV7vmVum4i0GobonOEbml/MPjYkGMnicjCj9Zu871UJu8gpTjz/8wnhDmbTKzyFOV35rQ48rX+CWm3QEFPl7KSmizvjUGNjB4O5NckthGJVuxU0iEkbjZxRcUIQXl8XmZngG2F93CB9+SQxKbjJplPi79eD2bOdTU3+xw+A6w+3r/X2eoQ50LLnJuyV4qz7YlxeBSFKDHANsqJXd4VkXd3T+z6nRkGKQcZOdBqF9qBAFg6Ju4Yns64yYlOHccW1XABEsBdPv3OAmMd6DwEKIJgUyULUru2fj8fbM1QN9RCl3gkHCTYbJOu3KjZctdwJiH6LxiHrWH+/PKhwNVP7UGNTcpB5/gii2S+O9RwflHEjIPFGSehOGMOaWNAYlNxkfm7EKx1b70JJOpvd9zWseWe0z3DnCYJtZUi67WbvcZHyMzhPNZR+EL791xKdKbc6O9T6KiMIQipRhdLnJI+Z49o/XQorh4SnIKoAsXQ3TcFmYgXii0eUm8RKDVn8mNKnpMMD9z+kVqRXiitINAxfYfhdZ1+F4yE3u1sJhuox6Q450X9L0BxCVoHGpZ3meQSUaa4JTHhEWnoVZ0PGkHW7StGIU75W+VLSzF09Ki/eJu6QJtzvmAr3NVuFXSYObVJshWbzXh+ONGQeIJ6shzH0JwJLS1xaaECJJ0u24yeZwG7vSMXqOI2bm7pWi/iOq4waPWXZswltFg5scfQzkkmw2KqZbpAiaIEM5U78rGO1wKQT9jXtu8sEUQkqoH8pNzaCyY4hT0GhrNRW5t+nJotqPR4+bDAWwqDAYXeiiwQpmsi989bo0g6MYB0/liBd/sOcm66Ko3hmvmLvrRYYhsgwI1Am+boTcazPiBELH/qPHTSa3nUYHLzCIBpV9AyWu+QyCGZZxgD/+zVUDZz3UWvHKFzbcZEYgCQ72XMqkC7abGMSDyqQP4yVjg1vyF97XmK5gpVDXc6A+0OZMvfMEbFU0/KGHdjDCQWXiYa0LHCqUvvwmV3ti0UNNLSTpYZCYbznW+xkG2aCyfKXKZ5x1Mkz6gUVDvA0NWtxket9FHtXFALKK4waVmZJhigEgkiuDQdR9wyvg1fWFJ2SgJlxyDNrn1+WDynyuEtvBFW8eYnh5tbGreP5xbTPd4bnXm5XuIB5UFhmcxWBcBWlTMGFOh6sel7/AIJxh+CxfKAaV5Rgs1wzPPA+kGFhn3sJgvPjqnjlEuQP6QWX5zq1YqMXDTmbhyDDAYXR2/zEGx6MIiVkEGPlEU3swZeB79FOyaSul/ehU9pJOVVbjgpvkq96/7KAy8xRVDuxYRybjRXglAWKtNCp7LPUiLBtstxMuSxJcf5NJAhhXl6LCx1jTZGBy511lb5fwE8XTdaeUsJrWGikeqjYcPdCBAV8uuaROZa/RXVq8YTRWwizVPRF35HIFmPEFOiunw6BR2YsKAvbFFAeqPBnN8LiYiOSNhBNoZM4PsxY2lT2EfQXJBhY9H/4TCDIQkCg5EID08RqjVdlLSwb5MQf7smtkwVsnEO1YwTlqW3gh9OejV9kjuS/vsXzJxnEiQiDZz7DrXwX1tF/mheuketes7CX7PXP/G7l3IBQpcI0bFS8Y2At/UG/LjQU32pW9lk2/1SrhzJflmZaoAqQCBmTlwXhJLH+wr+y5ctMRPi5e+TvctfADoTwKYcNp4ItgdvT7sKTq+DuMZmVPF+Ax5FEHwd08MwMRoiCou1MJJfTa8uoVpxUQlEcy2qBX2TNaFA451o9pTbMpm4KpdwSNQIgON6F1bIFBIs6ZnD/YVvasCIH1Ps8X6wAGA4e8M2KTSecuwpxPkvW5bip76aaAN/JaD8ExmRMEGETHy0C3XCA5uW4EWKT9zmVlr8Dg4G/xHlZzCcQcx1FjANV4c0Q9ua4nLl8MQxW1kspehQF+woCz9ZSnh5v6EvepDoKDeWVNDlTbHA0pyJaispdjgF3VrxuyXpeZbNqK/c7GKlVvhvc5V0Joqu/wPjFY35AsfFbZs9mqTKF+1K/fPQUnI7g2xyIUC6dcju9cF7jcEeYYF0bwBbo9V9lzSemKO/hj+jd4robDBAh5ACwgoC2mRV0X51u+O/FB0asWRpNJZc/+AltUTtsfnKv/s9d9ab6EcqCDwB6xBmZvzLYGIGy25NGrwUvAyZ9YAGpafLLxYsOYxN4qBTB0CDjVlCsJ0MWmS+w3VUZAcbIAty0q+DMiRHJS17v5Royh9akdfIR4yhbWWTyV52VBqySCShzyyl7BHBaZ/W6InPwU3iJtNHaRlEOk+MNsV4SX2lOxDNiVkktTiDTGYcUhbGrQn/PlgckyzsYIMwObMSzWkNrVo64RmpNAdxYevGLuzqezQPh2FuwgQMr3CAzo5jesGR7M1PlaA/VqlXfP59sUk8vFwyHMIPrBafFwwaPYN4iwDB+orB7G/A+0i8GS6nLIgu5hfSfXOStusvwNA6yKjIffHR8z3L1SZDwmz1td1he16eUxrK0pyq5zbRcOIhyyTTp8iYEtE7E4ozAvMKKwD25NBXmOmESfUgLRJfUTDKqONvqYkltwXrT9emOoHZfEgL8wIXtpI0RuuK1LxS+x8W7us0Qh+wi4J6HHG/orwqD8LiNOIvGXcT98xzJgaJO0ykKE/c4/5T3l3f2AgTxjGo1NW1JmPBRPaX2sbE34gy7+TTkHUn+XbAzxMdvqTMWjEbkoN0ubCHkmACJ8NC0EeIHBxkJ5eLkAdt57UgkYaKRzc0Si3JexdVk+Vvtsnw9BQH1nlBaa1KkZ7rUrPEQvUZUoqy4/5Pj5XQj5AE2/ph5hUBUnYxAY7/nYZDKBebL4VReu3gNxkR5Nyhr9Rxhg1u0iERhElvBm8gQ9NXSplGsMzrQLUU7CK9DB32HAIc5gYlyuoxgc+TBycto4Mg9KdW6h6w7uinHP9SsMDpXX06MTuYNXu9i1dYVXcKuRkGGnflAecRnyEQaMFi8CVyGL8EUfHKSSvsq8wcwtYBhy8juG90vHWcHVzFmjOvM7DDK6WtqxVDrYtuiQqtnTgDTefQQWf85VPYxCcboZjj/HwMldVK8Mgq1xjWxBHwdRHwx1qio95eHGf2KpKzCeYIAODc10BG2M4fT09zyLdVr1RQykWxsijabHgGdlQrU3SoopkN88xgC7JINFQIlLBvOpU6uewM9SrjhfiKi7wOATad47oXKIUv3PPgZxZrQZbxX1AKtBXhf5BSFRRDS0nOlLYYXBlYSLhHrYEZZeIrHHQOlHd9xteclZU16GPU9T4nm8kpD2YUt3hGsHpxZi6BYxKsq2mzdSJ3KV2reLAdZez7UUcM00E30qkbc9jJaXyfHUX3Ee5Q1HxrLnD4KawwMMqk5DnIzpuGIErPyU5/l7unvLoRC1/VmCwfSFwYUKcF2k0LNPkw6XI8Hgq14dLQyuhx1z/+b5jNeQ5FPyCzPapWxeJWJ2W/WjB2aK7pnCZbd4tkhrX/FBC6Jr+Tv08dq/v0W7yacBM8RijkFMJUHdHkz1bmiX8AYDM67n8I3UacPW97R3pzi47ovjktfKnq0chDO0SDCAqXEF5Fv7oPUs4fFqbzPYX00hl2tRjOI/NeCzNMRxvfnvCUJgXUw/kbxMBZsUVE9OMMXNtxj067UusmD4oVhPdU9j/vBi9+kLI69+bDHQK0BOoggrBU8HwOAZfLeEQI+qv0poGEdQViazN+CmIhQVbzHmUZZ08NQhNMqVm99lOoXi1F2448MxUEiVW8TEio9snrdSS/y0FJ5hABF0Xlui1uPCsE74ns6XxOYtDGYjAZdOQjjGeOQc7q/zvLY72ieP3zEYamyK7ddTYv1r6/h60GSYnF5JnpgVS8HmemuoiOnVfVOFeYfB1ZMKDfw98uyEf0WOAAsM5tF6XBd0SwHm0YVptp7zLzCA9uRTemPYAPizmc4+ITXERB8wNbmH0HOp4GDFHcI6qInnHeu/cmG8wmAGU645A2d8jmPRi5M2UmMkpSBRreIEA8jxZ3BZ/cuCdSIMbQ9KuB/tOilD4TDlJjef9NV7M5Af2aceR7yDzVGK0iOuWiXeYGBHgj3Ucy1edO0SF4lwvdgpQ50T4IRPZPTULDDgUAVsQS6uiBEvrUCPY9gtBNPJj7PrToeH1xnuWKQBdZPBlMMM3d1zPT0LDPTStysDr80gOw5cN6IxCfLJVci9z+s9D97FNWMIlJMAL+UqdhiAuivNjOobcnO42Zh3hwWkg2OSBw8qn7OVAktNt1ovoZ4SU3ASYYDrnFFpmQYDTjZWadgQdlPcYvOvU7o7UWRnoceAeHhCNm82net8PBEVy0gR6//OqgmGKFgsNcqNAe5AU0dI1B0rZg8sw08GpcKX53VWMcXpvAnTbyeYzPOAQk4a7KweiKJ37g9w78JHXHLmZuo1I2Z91xj+eEXZ+Pi7G8K+i2OeZH4dzbYwuCVMiH0i4hY3Mss3R4WBMYRfhFBrNj6voxVEsYkmf/wOeCEWRRU2qzLu2z93QXMox1IF76uLI5tN9A6CQPB8u+lvmz4vtvU6nJ6HSiBFNSG6v3DoiZnNejs7Fh7MKGUZ8wfRsVVxVF1puGY+qh/jPpsdOi5mMACVgrSbOZMr4iOTIvecFRsciqmopW0qrcFDczi2PTY7DM150q9b68OjM8Q2GLDl5fYdtnsmDkb4Bp+j4cm8tHdB4JCqwk3pfXOGwlERUoI/MAaBY56yLor7tHOY/j8w2JFuKIhJnmndkVg61WHz8af/PQY3g0YeqSh5zyxfoSUT3l2fNspQ9i7bNKo96I8xyFLpuwD5G6OIoBpzibbEkPLgt2q3F0xqQtsHEohEL3oP3V+rTUvLdNhgcC9+BlP+dvvRKVRYLDAehC/nAxbnlI6ecE792A4DeKm/aDoOBVo1BPlhEnjiA9h8pzEOzBNpfwFY9bfq4n10ENTZaf68MhjjiI0VWFdGZRYIammxLakESRT8cD8+d08iJoU2T9VouAPzKT1yODzqaWdJorylh68ed8P4KrrHO0j60sG3GMwyJ0bXDHRXdSw9KO4Hcm+AwOCkA74uHunqQ3FTLQxO7p70bgD5YV8tDNjEYOV6+piUryc9iR4CstWVe199Mm8dDBiewLz2xQflEnk/zzCYohtzg4oWv9QU3ASwxgiy76oE+uqPTbRLB9tLYWa9yyvCb/iVNwnHVuAwWUI/DEOBQcKvm2DvpEsfVIz8kRkYzX3BRgBQysA6HEd1zbGbdcpgVKcMkvisarYmdLhJ4DO9KX47ovwI4dkZh3JR+LxoqKNeCvLoDzlrY8bZFAdkditm8EXUN1/eYaFpAFRb8ti4RGmiZr4jvo5YHhbfxSAQFLh4EFmikApL2V8CVKKewY3y5XAYzN4/4dfahcMOBvZqaBApz/JRKtXIQL2JxQdWwagnRzv/a6wFt9OmO8ZbCB5hoA89hNUoIXq0pk80YWIqZR3FkaCvU3KDQRnYz62M1Fzl8hXfwKU6uyy+baaH5jzAgEYmpVU/rRXJ4ge5YnEPjnjvV0GXXQ/v88Z8lW+jJ7oMjD1ZAyQG3MT1R3iSh9uGeTzCQL3F3FRiBUpWuqNUL26sUmGAMpyLMKA++ujUHx5vMMh+yz8ysvfq4kz3IXUi7su1zh6xDv2hoi2ka2stwAiD1GTZFkFbDNDFAKvDeJMv2WYVo06xVOFfYZByYmUCYTHA5j5ChYT3SjQZrcHAcsZtkWEcCr34gkG0TUyuYcvcZc0jjJGyCK2NgZor+DMGi1p7eKSwE6yE3C9ktZyBUOtpnQCvkGuEGwE7i7fA+vN/AkYGnWJjnZK3MFhKrqzTJXwJv1lYNki3/Jsnr/TBL2FXnPbXG/8JBm6wUNn1wj/DAM3ibHipziu59kSgs1y5VCjLII+XIKCJATdSlRgDU4rk5mUkUSK7D/YShN4CQl/hEH2rc1Lckhx2H01K0Xa3iYdmcDyD4EhPo7bbAt/tjNwUFqNywBNTMExnDFqj0MnOtsA/8ojFDdBKNkXLd13+pW65xJP3oB0Dqm3hFQZd8ZkDgd6bM/e3IyqSpDzpVEb0NXHvgy+UxdUKhFvmGNS5lzAgKk2HIm8liOnslykGzwwB+ymdTBcelBdC922FGHAnZ2nYZ+vQxFd+AEhAoDlTfHsxVzRlVNx6ca8MCqUv6jXRs9+1ZcRmdx3PvkZR8CkGR0qmNtyWUJAF2zb/wgo4z9zmCCz6UvVA0tRor4WpZ+gX30cUuuszA0g9xX2qEYj9FYnh9fhz4AiC2PxQGNzkzFFLFGOimQ/eXWDtEoM8pUESHx35qjx/j0f8ficv9m0CuErgoRawOgJ5/MDEaCJtZgtIF7ipYd7yyqE3uChQ4LlN5VJj3vV/egy2Ey5eUSoM+HdzIlk0Wzn5YDNVwBW1uRP1zlksN0+N/vMHGOChG19/Y3+CaV7pGEMtkoLC79MWdLdtVZwB1gERS4L4alPCU0eeyjOD0VXZZNpJdp9e79rppPbvLD6XiSIEBMjdWugVdishudpWAhWwPdlmOFaod3k3Sal/J1DxVL81S9xHphpO3vTGDLK68PYY8XvDpUumXfIxJnN6NQmfAxOUmGcX+SAku9MYpMAgNpPxyhDG2qlGoI4NMYDwsDCZgNf/k9F5vhtKIKpHcWsGHFE5pHeePOSoVibm7DDgqqx8fcR1nk243QFGxx+/G2kcrDLFUS0FxG1qG86sc67IGvwEUYte3oDzyLd4v8OeSrqrxuAcARL3sLGojfojNvBgL2FOEHL5xDGPTpTh9t3RWwmS2eE9OWYd34DQTvNST8LHQYh6lQIDSUHp4ho3NCVqDHDcAxlFGPocA+Ss22MMrtLC2SiNQxTDp07UFlO4GVxSn317z4lQd4S4wlsFEsTT7bHagLEkJsGgK31pcdzhy7znMgMmGAz3W+hm/u3F4CgFLIbwHLio1gc8ebsXI2ODAWCnHlQYtIPmh90zEgFZRhctKEDU1o2Cqj6YHXFnMdDRfI0Bn2ePj9JMN81gT30VRVymR2ohfWLv4T1yaOZO7DoEcV9hmNvsX8s6xvYZcBgvhB4VmzxpzyLsl0K8k/cwSKDGHgMG5AVqDPhA99tYDLLFPa589zB4FoihSHvyYJm7ZDHdjto7Y2gIyu3jrzCg+57ad7KzRfbZYvJs/W5V25XzjZGIOrH8HYPZ7TAAJumEJYU6nq4bYZo7cwHzp5/JiX7/GoPB6puIvpNIVTPxAbtmEE2Fxji86PcxBqBdTkVBE9uSSVqX10NOmrfn3LVNfz+Dy9nBID6xWDEpQc1H2gI3Nfm4IIs0c0IPAx/DeokygVcYpBFEP86LovMgG4A+XCSMTR/kMvStvdcJFtvy6/bagYb+Rb0TqXKHR1paLRxCtBXSW9c4LIfQMAT8BQZRgrJhZxBHZXUvyl3tu0cV+nFLi6J9ECz/iEExdav6EKT4lGZgOF9vCPWRydmnoJbC5A+/nznWcRboxfJ2l7r5ZftG3flQL/2BWk7hHtLUfNmxyYEhoqkITKwHfq5jB4Nmf+UvxwMXHwZsyezCIbq/cIaAA34KdsQMIRrf9/qh1RClR2rXtHUgib64MlweiSEYDJAlwzbHn8Xax0eDR2pxtlGoXTflySSyuKNZF8N9cxyxuMWZgbTTSbunT/FUywu2UHhscSPAgGprIMIETI+WhmFxp1htSCvkUwRAKqkU9rYQz3joyGq1REbECHAcQ6RRkPhTynKMMujZaDQWAXKyxpmuqu0IUoPBgXuABpGcbmoW1JyFBUPsf4VwUAWqFdyS2yq9/WsUri6OCXDNysljrmvUOm2quebD1dIt+Rsecw7Vxg8zKeOafrNWmx9mAmwMITd3GGFHqbC8tDnRqgRLDOwxJJ4E1aqfeVwHzGtkHOXQVR/HoyBCq+NSoe35+Hk/Xq2FMdGPcxQwR5CcE2r8IFCGTCsD6pDvMMA5KT2IH3mdQ9LDgNgKpmDPOLyJEy7VrRNtjHjyyZoq9HICuztDMqDtjmOO2dtjsJtaejgB/5mATYqLPOvWfnmOYgIOyySpiwFTTQcVBqwwYJPjgZHc8HBLhlYvzN30l34QaDHQ9ByMk7+5N0yBWqHFQZeQpJ6YRn8+5ByVAO8OknEmoeVByDfWcAGaMwz1iW06np07ljj4NcUAJm5DQ16hth3Qqhtg3BULj1tmu/JP3BRWzMFRU1iCqbYbe1qglahDJWR2QuGZr7hPKKgolBhQnL1oLBOplDEM95iXl7GJ23zuZCobixYiYt/56DQqE0zJkUT6hhgmXUhVCVpmQKShqxj2kxZTk+oOsw2ED3QCrqog9zz9E0H9IlWnnFJsfR/BHWFqMo3qGkHoKcOm02UxEa8cxjc4eghxTrSoDfkeDS+iDl6Am8v6uQvMWr/cSjHCWSz3fgtX+j6q1ZBvWwgtB/6dh1vxsHZgHxEzyM47sUV6AyghelgJ+paaiEgs4wdjNECgNFrkQqWMSofVudBk8MM3UwXCm1Wfky7RCwLuv2FyDmTEa7QsYaZFGYWCPJumrVQcWwxAGhZQdIYPWnEZI5kEdPkflc61E9AmaX/WWmZrZ9Snhh/nrCrp8y/2w4VbIQbuhAZEZjn0u38VvYaVsRZLiIefkpcggrnnJ8+he849Bh/jpypFvMpiQs+wpc2BX84/NG3mgQwe51F2WmcpabH/CTAA9QYogmSFqccAAAAASUVORK5CYII=') repeat; color: #2D3841;
+ font-weight: 100;
+ margin: 0;
+ }
+
+ table
+ {
+ width: 100%%;
+ color: #BDC8D1;
+ }
+
+ table th,
+ table td
+ {
+ padding: 8px;
+ }
+
+ table tr:hover td
+ {
+ background-color: #393939;
+ }
+
+ table th
+ {
+ background-color: #191919;
+ text-align: left !important;
+ }
+
+ #header
+ {
+ clear: both;
+ padding: 0px 0px 10px 20px;
+ height: 150px;
+ }
+
+ #wrapper
+ {
+ margin: auto;
+ width: 800px;
+ color: #BDC8D1;
+ }
+
+ #wrapper h1
+ {
+ background-color: #191919;
+ color: white;
+ letter-spacing: 2px;
+ margin: 0px;
+ padding: 8px 15px;
+ font-family: 'Lucida Grande',Calibri,Verdana,sans-serif;
+ font-weight: 100;
+ }
+
+ .page-content {
+ padding: 20px;
+ background-color:#232323;
+ }
+ </style>
+ </head>
+ <body>
+ <div id="wrapper">
+ <h1>Kivy Styleguide (PEP8) Check</h1>
+ <div class="page-content">
+
diff --git a/sbapp/plyer/tools/pep8checker/pep8kivy.py b/sbapp/plyer/tools/pep8checker/pep8kivy.py
new file mode 100644
index 00000000..60ec4e7f
--- /dev/null
+++ b/sbapp/plyer/tools/pep8checker/pep8kivy.py
@@ -0,0 +1,116 @@
+import sys
+from os import walk
+from os.path import isdir, join, abspath, dirname
+import pep8
+import time
+
+htmlmode = False
+
+pep8_ignores = (
+ # continuation line does not distinguish itself from next logical line
+ 'E125',
+ 'E126', # continuation line over-indented for hanging indent
+ 'E127', # continuation line over-indented for visual indent
+ 'E128') # continuation line under-indented for visual indent
+
+
+class KivyStyleChecker(pep8.Checker):
+ def __init__(self, filename):
+ pep8.Checker.__init__(self, filename, ignore=pep8_ignores)
+
+ def report_error(self, line_number, offset, text, check):
+ if htmlmode is False:
+ return pep8.Checker.report_error(
+ self, line_number,
+ offset, text, check
+ )
+
+ # html generation
+ print('<tr><td>{0}</td><td>{1}</td></tr>'.format(line_number, text))
+
+
+if __name__ == '__main__':
+
+ def usage():
+ print('Usage: python pep8kivy.py [-html] <file_or_folder_to_check>*')
+ print('Folders will be checked recursively.')
+ sys.exit(1)
+
+ if len(sys.argv) < 2:
+ usage()
+ if sys.argv[1] == '-html':
+ if len(sys.argv) < 3:
+ usage()
+ else:
+ htmlmode = True
+ targets = sys.argv[-1].split()
+ elif sys.argv == 2:
+ targets = sys.argv[-1]
+ else:
+ targets = sys.argv[-1].split()
+
+ def check(fn):
+ try:
+ checker = KivyStyleChecker(fn)
+ except IOError:
+ # File couldn't be opened, so was deleted apparently.
+ # Don't check deleted files.
+ return 0
+ return checker.check_all()
+
+ errors = 0
+ exclude_dirs = ['/lib', '/coverage', '/pep8', '/doc']
+ exclude_files = ['kivy/gesture.py', 'osx/build.py', 'win32/build.py',
+ 'kivy/tools/stub-gl-debug.py',
+ 'kivy/modules/webdebugger.py',
+ 'kivy/modules/_webdebugger.py']
+ for target in targets:
+ if isdir(target):
+ if htmlmode:
+ path = join(dirname(abspath(__file__)), 'pep8base.html')
+ print(open(path, 'r').read())
+ print(
+ '''<p>Generated: %s</p><table>'''
+ '' % (time.strftime('%c'))
+ )
+
+ for dirpath, dirnames, filenames in walk(target):
+ cont = False
+ for pat in exclude_dirs:
+ if pat in dirpath:
+ cont = True
+ break
+ if cont:
+ continue
+ for filename in filenames:
+ if not filename.endswith('.py'):
+ continue
+ cont = False
+ complete_filename = join(dirpath, filename)
+ for pat in exclude_files:
+ if complete_filename.endswith(pat):
+ cont = True
+ if cont:
+ continue
+
+ if htmlmode:
+ print(
+ '<tr><th colspan="2">%s</td></tr>'
+ '' % complete_filename
+ )
+ errors += check(complete_filename)
+
+ if htmlmode:
+ print('</div></div></table></body></html>')
+
+ else:
+ # Got a single file to check
+ for pat in exclude_dirs + exclude_files:
+ if pat in target:
+ break
+ else:
+ if target.endswith('.py'):
+ errors += check(target)
+
+ # If errors is 0 we return with 0. That's just fine.
+ sys.exit(errors)
diff --git a/sbapp/plyer/tools/pep8checker/pre-commit.githook b/sbapp/plyer/tools/pep8checker/pre-commit.githook
new file mode 100755
index 00000000..23d119c0
--- /dev/null
+++ b/sbapp/plyer/tools/pep8checker/pre-commit.githook
@@ -0,0 +1,78 @@
+#!/usr/bin/env python
+
+'''
+ Kivy Git Pre-Commit Hook to Enforce Styleguide
+ ==============================================
+
+ This script is not supposed to be run directly.
+ Instead, copy it to your kivy/.git/hooks/ directory, call it 'pre-commit'
+ and make it executable.
+
+ If you attempt to commit, git will run this script, which in turn will run
+ the styleguide checker over your code and abort the commit if there are any
+ errors. If that happens, please fix & retry.
+
+ To install::
+
+ cp kivy/tools/pep8checker/pre-commit.githook .git/hooks/pre-commit
+ chmod +x .git/hooks/pre-commit
+'''
+
+import sys, os
+from os.path import dirname, abspath, sep, join
+from subprocess import call, Popen, PIPE
+
+curdir = dirname(abspath(__file__))
+kivydir = sep.join(curdir.split(sep)[:-2])
+srcdir = join(kivydir, 'kivy')
+script = join(srcdir, 'tools', 'pep8checker', 'pep8kivy.py')
+try:
+ with open(script): pass
+except IOError:
+ # if this not the kivy project, find the script file in the kivy project
+ os.environ['KIVY_NO_CONSOLELOG'] = '1'
+ import kivy
+ script = join(dirname(kivy.__file__), 'tools', 'pep8checker', 'pep8kivy.py')
+ srcdir = ''
+
+# Only check the files that were staged
+#proc = Popen(['git', 'diff', '--cached', '--name-only', 'HEAD'], stdout=PIPE)
+#targets = [join(kivydir, target) for target in proc.stdout]
+
+# Correction: only check the files that were staged, but do not include
+# deleted files.
+proc = Popen(['git', 'diff', '--cached', '--name-status', 'HEAD'], stdout=PIPE)
+proc.wait()
+
+# This gives output like the following:
+#
+# A examples/widgets/lists/list_simple_in_kv.py
+# A examples/widgets/lists/list_simple_in_kv_2.py
+# D kivy/uix/observerview.py
+#
+# So check for D entries and remove them from targets.
+#
+targets = []
+for target in proc.stdout:
+ parts = [p.strip() for p in target.split()]
+ if parts[0] != 'D':
+ targets.append(join(kivydir, target.decode(encoding='UTF-8')))
+
+# Untested possibility: After making the changes above for removing deleted
+# files from targets, saw also where the git diff call could be:
+#
+# git diff --cached --name-only --diff-filter=ACM
+# (leaving off D)
+#
+# and we could then remove the special handling in python for targets above.
+
+call(['git', 'stash', 'save', '--keep-index', '--quiet'])
+retval = call([sys.executable, script, srcdir] + targets)
+call(['git', 'stash', 'pop', '--quiet'])
+
+if retval:
+ # There are styleguide violations
+ print("Error:", retval, "styleguide violation(s) encountered!")
+ print("Your commit has been aborted. Please fix the violations and retry.")
+ sys.exit(retval)
+
diff --git a/sbapp/plyer/utils.py b/sbapp/plyer/utils.py
new file mode 100644
index 00000000..101763e9
--- /dev/null
+++ b/sbapp/plyer/utils.py
@@ -0,0 +1,290 @@
+'''
+Utils
+=====
+
+'''
+__all__ = ('platform', 'reify', 'deprecated')
+
+from os import environ
+from os import path
+from sys import platform as _sys_platform
+import sys
+
+
+class Platform:
+ '''
+ Refactored to class to allow module function to be replaced
+ with module variable.
+ '''
+
+ def __init__(self):
+ self._platform_ios = None
+ self._platform_android = None
+
+ def __eq__(self, other):
+ return other == self._get_platform()
+
+ def __ne__(self, other):
+ return other != self._get_platform()
+
+ def __str__(self):
+ return self._get_platform()
+
+ def __repr__(self):
+ return 'platform name: \'{platform}\' from: \n{instance}'.format(
+ platform=self._get_platform(),
+ instance=super().__repr__()
+ )
+
+ def __hash__(self):
+ return self._get_platform().__hash__()
+
+ def _get_platform(self):
+
+ if self._platform_android is None:
+ # sys.getandroidapilevel is defined as of Python 3.7
+ # ANDROID_ARGUMENT and ANDROID_PRIVATE are 2 environment variables
+ # from python-for-android project
+ self._platform_android = hasattr(sys, 'getandroidapilevel') or \
+ 'ANDROID_ARGUMENT' in environ
+
+ if self._platform_ios is None:
+ self._platform_ios = (environ.get('KIVY_BUILD', '') == 'ios')
+
+ # On android, _sys_platform return 'linux2', so prefer to check the
+ # import of Android module than trying to rely on _sys_platform.
+
+ if self._platform_android is True:
+ return 'android'
+ elif self._platform_ios is True:
+ return 'ios'
+ elif _sys_platform in ('win32', 'cygwin'):
+ return 'win'
+ elif _sys_platform == 'darwin':
+ return 'macosx'
+ elif _sys_platform[:5] == 'linux':
+ return 'linux'
+ return 'unknown'
+
+
+platform = Platform()
+
+
+class Proxy:
+ '''
+ Based on http://code.activestate.com/recipes/496741-object-proxying
+ version by Tomer Filiba, PSF license.
+ '''
+
+ __slots__ = ['_obj', '_name', '_facade']
+
+ def __init__(self, name, facade):
+ object.__init__(self)
+ object.__setattr__(self, '_obj', None)
+ object.__setattr__(self, '_name', name)
+ object.__setattr__(self, '_facade', facade)
+
+ def _ensure_obj(self):
+ obj = object.__getattribute__(self, '_obj')
+ if obj:
+ return obj
+ # do the import
+ try:
+ name = object.__getattribute__(self, '_name')
+ module = 'plyer.platforms.{}.{}'.format(
+ platform, name)
+ mod = __import__(module, fromlist='.')
+ obj = mod.instance()
+ except Exception:
+ import traceback
+ traceback.print_exc()
+ facade = object.__getattribute__(self, '_facade')
+ obj = facade()
+
+ object.__setattr__(self, '_obj', obj)
+ return obj
+
+ def __getattribute__(self, name):
+ result = None
+
+ if name == '__doc__':
+ return result
+
+ # run _ensure_obj func, result in _obj
+ object.__getattribute__(self, '_ensure_obj')()
+
+ # return either Proxy instance or platform-dependent implementation
+ result = getattr(object.__getattribute__(self, '_obj'), name)
+ return result
+
+ def __delattr__(self, name):
+ object.__getattribute__(self, '_ensure_obj')()
+ delattr(object.__getattribute__(self, '_obj'), name)
+
+ def __setattr__(self, name, value):
+ object.__getattribute__(self, '_ensure_obj')()
+ setattr(object.__getattribute__(self, '_obj'), name, value)
+
+ def __bool__(self):
+ object.__getattribute__(self, '_ensure_obj')()
+ return bool(object.__getattribute__(self, '_obj'))
+
+ def __str__(self):
+ object.__getattribute__(self, '_ensure_obj')()
+ return str(object.__getattribute__(self, '_obj'))
+
+ def __repr__(self):
+ object.__getattribute__(self, '_ensure_obj')()
+ return repr(object.__getattribute__(self, '_obj'))
+
+
+def whereis_exe(program):
+ ''' Tries to find the program on the system path.
+ Returns the path if it is found or None if it's not found.
+ '''
+ path_split = ';' if platform == 'win' else ':'
+ for pth in environ.get('PATH', '').split(path_split):
+ folder = path.isdir(path.join(pth, program))
+ available = path.exists(path.join(pth, program))
+ if available and not folder:
+ return path.join(pth, program)
+ return None
+
+
+class reify:
+ '''
+ Put the result of a method which uses this (non-data) descriptor decorator
+ in the instance dict after the first call, effectively replacing the
+ decorator with an instance variable.
+
+ It acts like @property, except that the function is only ever called once;
+ after that, the value is cached as a regular attribute. This gives you lazy
+ attribute creation on objects that are meant to be immutable.
+
+ Taken from the `Pyramid project <https://pypi.python.org/pypi/pyramid/>`_.
+
+ To use this as a decorator::
+
+ @reify
+ def lazy(self):
+ ...
+ return hard_to_compute_int
+ first_time = self.lazy # lazy is reify obj, reify.__get__() runs
+ second_time = self.lazy # lazy is hard_to_compute_int
+ '''
+
+ def __init__(self, func):
+ self.func = func
+ self.__doc__ = func.__doc__
+
+ def __get__(self, inst, cls):
+ if inst is None:
+ return self
+ retval = self.func(inst)
+ setattr(inst, self.func.__name__, retval)
+ return retval
+
+
+def deprecated(obj):
+ '''
+ This is a decorator which can be used to mark functions and classes as
+ deprecated. It will result in a warning being emitted when a deprecated
+ function is called or a new instance of a class created.
+
+ In case of classes, the warning is emitted before the __new__ method
+ of the decorated class is called, therefore a way before the __init__
+ method itself.
+ '''
+
+ import warnings
+ from inspect import stack
+ from functools import wraps
+ from types import FunctionType, MethodType
+
+ new_obj = None
+
+ # wrap a function into a function emitting a deprecated warning
+ if isinstance(obj, FunctionType):
+
+ @wraps(obj)
+ def new_func(*args, **kwargs):
+ # get the previous stack frame and extract file, line and caller
+ # stack() -> caller()
+ call_file, call_line, caller = stack()[1][1:4]
+
+ # assemble warning
+ warning = (
+ 'Call to deprecated function {} in {} line {}. '
+ 'Called from {} line {}'
+ ' by {}().\n'.format(
+ obj.__name__,
+ obj.__code__.co_filename,
+ obj.__code__.co_firstlineno + 1,
+ call_file, call_line, caller
+ )
+ )
+
+ warnings.warn('[{}] {}'.format('WARNING', warning))
+
+ # if there is a docstring present, emit docstring too
+ if obj.__doc__:
+ warnings.warn(obj.__doc__)
+
+ # return function wrapper
+ return obj(*args, **kwargs)
+ new_obj = new_func
+
+ # wrap a class into a class emitting a deprecated warning
+ # obj is class, type(obj) is metaclass, metaclasses inherit from type
+ elif isinstance(type(obj), type):
+ # we have an access to the metaclass instance (class) and need to print
+ # the warning when a class instance (object) is created with __new__
+ # i.e. when calling Class()
+
+ def obj_new(cls, child, *args, **kwargs):
+ '''
+ Custom metaclass instance's __new__ method with deprecated warning.
+ Calls the original __new__ method afterwards.
+ '''
+ # get the previous stack frame and extract file, line and caller
+ # stack() -> caller()
+ call_file, call_line, caller = stack()[1][1:4]
+ loc_file = obj.__module__
+
+ warnings.warn(
+ '[{}] Creating an instance of a deprecated class {} in {}.'
+ ' Called from {} line {} by {}().\n'.format(
+ 'WARNING', obj.__name__, loc_file,
+ call_file, call_line, caller
+ )
+ )
+
+ # if there is a docstring present, emit docstring too
+ if obj.__doc__:
+ warnings.warn(obj.__doc__)
+
+ # make sure nothing silly gets into the function
+ assert obj is cls
+
+ # we are creating a __new__ for a class that inherits from
+ # a deprecated class, therefore in this particular case
+ # MRO is (child, cls, object) > (cls, object)
+ if len(child.__mro__) > len(cls.__mro__):
+ assert cls is child.__mro__[1], (cls.__mro__, child.__mro__)
+
+ # we are creating __new__ directly for the deprecated class
+ # therefore MRO is the same for parent and child class
+ elif len(child.__mro__) == len(cls.__mro__):
+ assert cls is child
+
+ # return the class back with the extended __new__ method
+ return obj.__old_new__(child)
+
+ # back up the old __new__ method and create an extended
+ # __new__ method that emits deprecated warnings
+ obj.__old_new__ = obj.__new__
+ obj.__new__ = MethodType(obj_new, obj)
+ new_obj = obj
+
+ # return a function wrapper or an extended class
+ return new_obj
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────